Merge branch 'main' into feat/meshcentral
This commit is contained in:
commit
6d8f85501f
|
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="WEB_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$" />
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/TTMT.ManageWebGUI.iml" filepath="$PROJECT_DIR$/.idea/TTMT.ManageWebGUI.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="AutoImportSettings">
|
|
||||||
<option name="autoReloadType" value="SELECTIVE" />
|
|
||||||
</component>
|
|
||||||
<component name="ChangeListManager">
|
|
||||||
<list default="true" id="94f75d5b-3c15-4d7e-9da7-ad6f5328faf8" name="Changes" comment="">
|
|
||||||
<change beforePath="$PROJECT_DIR$/src/hooks/useAuth.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/hooks/useAuth.tsx" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/src/routeTree.gen.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/routeTree.gen.ts" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/src/routes/_auth/blacklist/index.tsx" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/src/routes/_auth/command/index.tsx" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/src/routes/_auth/room/$roomName/index.tsx" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/src/routes/_auth/room/index.tsx" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/src/types/app-sidebar.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/types/app-sidebar.ts" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/src/types/permission.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/types/permission.ts" afterDir="false" />
|
|
||||||
</list>
|
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
||||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
|
||||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
|
||||||
</component>
|
|
||||||
<component name="Git.Settings">
|
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectColorInfo"><![CDATA[{
|
|
||||||
"associatedIndex": 1
|
|
||||||
}]]></component>
|
|
||||||
<component name="ProjectId" id="3AQVfIkiaizPRlnpMICDG3COfJV" />
|
|
||||||
<component name="ProjectViewState">
|
|
||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
|
||||||
<option name="showLibraryContents" value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="PropertiesComponent"><![CDATA[{
|
|
||||||
"keyToString": {
|
|
||||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
|
||||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
|
||||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
|
||||||
"dart.analysis.tool.window.visible": "false",
|
|
||||||
"git-widget-placeholder": "main",
|
|
||||||
"ignore.virus.scanning.warn.message": "true",
|
|
||||||
"javascript.preferred.runtime.type.id": "node",
|
|
||||||
"node.js.detected.package.eslint": "true",
|
|
||||||
"node.js.detected.package.tslint": "true",
|
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
|
||||||
"node.js.selected.package.tslint": "(autodetect)",
|
|
||||||
"nodejs_package_manager_path": "npm",
|
|
||||||
"ts.external.directory.path": "D:\\MyProject\\NAVISProject\\TTMT.ManageWebGUI\\node_modules\\typescript\\lib",
|
|
||||||
"vue.rearranger.settings.migration": "true"
|
|
||||||
}
|
|
||||||
}]]></component>
|
|
||||||
<component name="SharedIndexes">
|
|
||||||
<attachedChunks>
|
|
||||||
<set>
|
|
||||||
<option value="bundled-js-predefined-d6986cc7102b-9b0f141eb926-JavaScript-WS-253.31033.133" />
|
|
||||||
</set>
|
|
||||||
</attachedChunks>
|
|
||||||
</component>
|
|
||||||
<component name="TaskManager">
|
|
||||||
<task active="true" id="Default" summary="Default task">
|
|
||||||
<changelist id="94f75d5b-3c15-4d7e-9da7-ad6f5328faf8" name="Changes" comment="" />
|
|
||||||
<created>1772524885874</created>
|
|
||||||
<option name="number" value="Default" />
|
|
||||||
<option name="presentableId" value="Default" />
|
|
||||||
<updated>1772524885874</updated>
|
|
||||||
<workItem from="1772524887267" duration="1839000" />
|
|
||||||
</task>
|
|
||||||
<servers />
|
|
||||||
</component>
|
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
|
||||||
<option name="version" value="3" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
|
|
@ -1,174 +0,0 @@
|
||||||
# API Services Documentation
|
|
||||||
|
|
||||||
Tất cả logic gọi API đã được tách riêng vào folder `services`. Mỗi service tương ứng với một nhóm API.
|
|
||||||
|
|
||||||
## Cấu trúc Services
|
|
||||||
|
|
||||||
```
|
|
||||||
src/services/
|
|
||||||
├── index.ts # Export tất cả services
|
|
||||||
├── auth.service.ts # API xác thực
|
|
||||||
├── app-version.service.ts # API quản lý phần mềm
|
|
||||||
├── device-comm.service.ts # API thiết bị
|
|
||||||
├── command.service.ts # API lệnh
|
|
||||||
└── device.service.ts # Helper functions
|
|
||||||
```
|
|
||||||
|
|
||||||
## Cách Sử Dụng
|
|
||||||
|
|
||||||
### 1. Auth Service (Xác thực)
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { authService } from '@/services'
|
|
||||||
|
|
||||||
// Đăng nhập
|
|
||||||
const response = await authService.login({
|
|
||||||
username: 'user',
|
|
||||||
password: 'pass'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Đăng xuất
|
|
||||||
await authService.logout()
|
|
||||||
|
|
||||||
// Kiểm tra session
|
|
||||||
const pingResult = await authService.ping(token)
|
|
||||||
|
|
||||||
// Thay đổi mật khẩu
|
|
||||||
await authService.changePassword({
|
|
||||||
currentPassword: 'old',
|
|
||||||
newPassword: 'new'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Tạo tài khoản mới (admin)
|
|
||||||
await authService.createAccount({
|
|
||||||
userName: 'newuser',
|
|
||||||
password: 'pass',
|
|
||||||
name: 'John Doe',
|
|
||||||
roleId: 1,
|
|
||||||
accessBuildings: [1, 2, 3]
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. App Version Service (Quản lý phần mềm)
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { appVersionService } from '@/services'
|
|
||||||
|
|
||||||
// Lấy danh sách agent
|
|
||||||
const agents = await appVersionService.getAgentVersion()
|
|
||||||
|
|
||||||
// Lấy danh sách phần mềm
|
|
||||||
const software = await appVersionService.getSoftwareList()
|
|
||||||
|
|
||||||
// Upload file
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('file', fileInput.files[0])
|
|
||||||
await appVersionService.uploadSoftware(formData, (progressEvent) => {
|
|
||||||
console.log(`Progress: ${progressEvent.loaded}/${progressEvent.total}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Lấy blacklist
|
|
||||||
const blacklist = await appVersionService.getBlacklist()
|
|
||||||
|
|
||||||
// Thêm vào blacklist
|
|
||||||
await appVersionService.addBlacklist({ appId: 1, reason: 'virus' })
|
|
||||||
|
|
||||||
// Xóa khỏi blacklist
|
|
||||||
await appVersionService.deleteBlacklist(1)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Device Comm Service (Thiết bị)
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { deviceCommService } from '@/services'
|
|
||||||
|
|
||||||
// Lấy tất cả thiết bị
|
|
||||||
const allDevices = await deviceCommService.getAllDevices()
|
|
||||||
|
|
||||||
// Lấy danh sách phòng
|
|
||||||
const rooms = await deviceCommService.getRoomList()
|
|
||||||
|
|
||||||
// Lấy thiết bị trong phòng
|
|
||||||
const devices = await deviceCommService.getDeviceFromRoom('Room A')
|
|
||||||
|
|
||||||
// Gửi lệnh
|
|
||||||
await deviceCommService.sendCommand('Room A', {
|
|
||||||
command: 'dir'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Cập nhật agent
|
|
||||||
await deviceCommService.updateAgent('Room A', { version: '1.0.0' })
|
|
||||||
|
|
||||||
// Cài đặt MSI
|
|
||||||
await deviceCommService.installMsi('Room A', { msiFileId: 1 })
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Command Service (Lệnh)
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { commandService } from '@/services'
|
|
||||||
|
|
||||||
// Lấy danh sách lệnh
|
|
||||||
const commands = await commandService.getCommandList()
|
|
||||||
|
|
||||||
// Thêm lệnh
|
|
||||||
await commandService.addCommand({ name: 'cmd1', command: 'echo hello' })
|
|
||||||
|
|
||||||
// Cập nhật lệnh
|
|
||||||
await commandService.updateCommand(1, { name: 'cmd1 updated' })
|
|
||||||
|
|
||||||
// Xóa lệnh
|
|
||||||
await commandService.deleteCommand(1)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Sử dụng với React Query/Hooks
|
|
||||||
|
|
||||||
### Cách cũ (trực tiếp gọi từ component):
|
|
||||||
```tsx
|
|
||||||
const { data } = useQueryData({
|
|
||||||
queryKey: ["software-version"],
|
|
||||||
url: API_ENDPOINTS.APP_VERSION.GET_SOFTWARE,
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Cách mới (tách biệt logic):
|
|
||||||
|
|
||||||
Có thể tạo custom hooks bao quanh services:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// hooks/useGetSoftware.ts
|
|
||||||
import { useQuery } from '@tanstack/react-query'
|
|
||||||
import { appVersionService } from '@/services'
|
|
||||||
|
|
||||||
export function useGetSoftware() {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ['software-version'],
|
|
||||||
queryFn: () => appVersionService.getSoftwareList(),
|
|
||||||
staleTime: 60 * 1000,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Sau đó sử dụng trong component:
|
|
||||||
```tsx
|
|
||||||
function AppsComponent() {
|
|
||||||
const { data, isLoading } = useGetSoftware()
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Lợi ích của cách sử dụng mới
|
|
||||||
|
|
||||||
1. **Tách biệt logic** - Logic API nằm riêng, dễ bảo trì
|
|
||||||
2. **Tái sử dụng** - Có thể sử dụng service từ bất kỳ nơi
|
|
||||||
3. **Dễ test** - Dễ mock services khi viết unit tests
|
|
||||||
4. **Centralized error handling** - Có thể xử lý lỗi chung
|
|
||||||
5. **Type safety** - TypeScript types cho tất cả API requests/responses
|
|
||||||
|
|
||||||
## Cải tiến trong tương lai
|
|
||||||
|
|
||||||
Có thể thêm:
|
|
||||||
- Global error handling middleware trong axios
|
|
||||||
- Request/response interceptors cho authentication
|
|
||||||
- Retry logic cho failed requests
|
|
||||||
- Request cancellation
|
|
||||||
|
|
@ -1,328 +0,0 @@
|
||||||
# Khác biệt giữa Services và Query Hooks
|
|
||||||
|
|
||||||
## Tóm tắt nhanh
|
|
||||||
|
|
||||||
| Aspect | Services | Query Hooks |
|
|
||||||
|--------|----------|-------------|
|
|
||||||
| **Location** | `src/services/` | `src/hooks/queries/` |
|
|
||||||
| **Mục đích** | Gọi API trực tiếp | Wrapper TanStack Query |
|
|
||||||
| **Caching** | ❌ Không | ✅ Có |
|
|
||||||
| **Background Refetch** | ❌ Không | ✅ Có |
|
|
||||||
| **Auto Invalidation** | ❌ Không | ✅ Có |
|
|
||||||
| **Type** | Async functions | React Hooks |
|
|
||||||
| **Dùng trong** | Non-React code, utilities | React components |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Chi tiết Từng Layer
|
|
||||||
|
|
||||||
### 1. Services Layer (`src/services/`)
|
|
||||||
|
|
||||||
**Mục đích:** Đơn thuần gọi API và trả về dữ liệu
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// app-version.service.ts
|
|
||||||
export async function getSoftwareList(): Promise<Version[]> {
|
|
||||||
const response = await axios.get<Version[]>(
|
|
||||||
API_ENDPOINTS.APP_VERSION.GET_SOFTWARE
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Đặc điểm:**
|
|
||||||
- ✅ Pure async functions
|
|
||||||
- ✅ Không phụ thuộc vào React
|
|
||||||
- ✅ Có thể sử dụng ở bất kỳ đâu (utils, servers, non-React code)
|
|
||||||
- ❌ Không có caching
|
|
||||||
- ❌ Phải tự quản lý state loading/error
|
|
||||||
- ❌ Phải tự gọi lại khi dữ liệu thay đổi
|
|
||||||
|
|
||||||
**Khi nào dùng:**
|
|
||||||
```typescript
|
|
||||||
// Dùng trong utility functions
|
|
||||||
export async function initializeApp() {
|
|
||||||
const software = await appVersionService.getSoftwareList();
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hoặc trong services khác
|
|
||||||
export async function validateSoftware() {
|
|
||||||
const list = await appVersionService.getSoftwareList();
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. Query Hooks Layer (`src/hooks/queries/`)
|
|
||||||
|
|
||||||
**Mục đích:** Wrapper TanStack Query bên trên services
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// useAppVersionQueries.ts
|
|
||||||
export function useGetSoftwareList(enabled = true) {
|
|
||||||
return useQuery<Version[]>({
|
|
||||||
queryKey: ["app-version", "software"],
|
|
||||||
queryFn: () => appVersionService.getSoftwareList(),
|
|
||||||
enabled,
|
|
||||||
staleTime: 60 * 1000, // 1 minute
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Đặc điểm:**
|
|
||||||
- ✅ React hooks
|
|
||||||
- ✅ Automatic caching
|
|
||||||
- ✅ Background refetching
|
|
||||||
- ✅ Automatic invalidation sau mutations
|
|
||||||
- ✅ Built-in loading/error states
|
|
||||||
- ✅ Deduplication (gộp requests giống nhau)
|
|
||||||
- ❌ Chỉ dùng được trong React components
|
|
||||||
- ❌ Phức tạp hơn services
|
|
||||||
|
|
||||||
**Khi nào dùng:**
|
|
||||||
```typescript
|
|
||||||
// Dùng trong React components
|
|
||||||
function SoftwareList() {
|
|
||||||
const { data: software, isLoading } = useGetSoftwareList()
|
|
||||||
|
|
||||||
if (isLoading) return <div>Loading...</div>
|
|
||||||
return software?.map(item => <div>{item.name}</div>)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## So sánh cụ thể
|
|
||||||
|
|
||||||
### Ví dụ 1: Lấy danh sách
|
|
||||||
|
|
||||||
**Service - Raw API call:**
|
|
||||||
```typescript
|
|
||||||
// services/app-version.service.ts
|
|
||||||
export async function getSoftwareList(): Promise<Version[]> {
|
|
||||||
const response = await axios.get(API_ENDPOINTS.APP_VERSION.GET_SOFTWARE);
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Hook - TanStack Query wrapper:**
|
|
||||||
```typescript
|
|
||||||
// hooks/queries/useAppVersionQueries.ts
|
|
||||||
export function useGetSoftwareList(enabled = true) {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["app-version", "software"],
|
|
||||||
queryFn: () => appVersionService.getSoftwareList(),
|
|
||||||
staleTime: 60 * 1000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Sử dụng trong component:**
|
|
||||||
```typescript
|
|
||||||
function Component() {
|
|
||||||
// ❌ KHÔNG nên dùng service trực tiếp
|
|
||||||
const [data, setData] = useState<Version[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
useEffect(() => {
|
|
||||||
appVersionService.getSoftwareList().then(d => {
|
|
||||||
setData(d);
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// ✅ NÊN dùng hook thay vì
|
|
||||||
const { data, isLoading } = useGetSoftwareList();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Ví dụ 2: Upload file
|
|
||||||
|
|
||||||
**Service:**
|
|
||||||
```typescript
|
|
||||||
// services/app-version.service.ts
|
|
||||||
export async function uploadSoftware(
|
|
||||||
formData: FormData,
|
|
||||||
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void
|
|
||||||
): Promise<{ message: string }> {
|
|
||||||
return axios.post(API_ENDPOINTS.APP_VERSION.UPLOAD, formData, {
|
|
||||||
onUploadProgress,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Hook:**
|
|
||||||
```typescript
|
|
||||||
// hooks/queries/useAppVersionQueries.ts
|
|
||||||
export function useUploadSoftware() {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: (data: { formData: FormData; onUploadProgress?: ... }) =>
|
|
||||||
appVersionService.uploadSoftware(data.formData, data.onUploadProgress),
|
|
||||||
onSuccess: () => {
|
|
||||||
// Tự động invalidate software list
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ["app-version", "software"],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Sử dụng:**
|
|
||||||
```typescript
|
|
||||||
function UploadForm() {
|
|
||||||
const uploadMutation = useUploadSoftware();
|
|
||||||
|
|
||||||
const handleUpload = async (file: File) => {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("file", file);
|
|
||||||
|
|
||||||
await uploadMutation.mutateAsync({
|
|
||||||
formData,
|
|
||||||
onUploadProgress: (e) => console.log(`${e.loaded}/${e.total}`)
|
|
||||||
});
|
|
||||||
|
|
||||||
// ✅ Software list tự động update
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button disabled={uploadMutation.isPending}>
|
|
||||||
{uploadMutation.isPending ? "Uploading..." : "Upload"}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────┐
|
|
||||||
│ React Component │
|
|
||||||
│ (SoftwareList, UploadForm, etc.) │
|
|
||||||
└──────────────┬──────────────────────────┘
|
|
||||||
│
|
|
||||||
│ uses
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────┐
|
|
||||||
│ Query Hooks (src/hooks/queries/) │
|
|
||||||
│ - useGetSoftwareList() │
|
|
||||||
│ - useUploadSoftware() │
|
|
||||||
│ - useDeleteBlacklist() │
|
|
||||||
│ - Features: │
|
|
||||||
│ - Caching │
|
|
||||||
│ - Auto invalidation │
|
|
||||||
│ - Loading states │
|
|
||||||
└──────────────┬──────────────────────────┘
|
|
||||||
│
|
|
||||||
│ wraps
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────┐
|
|
||||||
│ Service Functions (src/services/) │
|
|
||||||
│ - getSoftwareList() │
|
|
||||||
│ - uploadSoftware() │
|
|
||||||
│ - deleteBlacklist() │
|
|
||||||
│ - Features: │
|
|
||||||
│ - Pure async functions │
|
|
||||||
│ - Direct API calls │
|
|
||||||
└──────────────┬──────────────────────────┘
|
|
||||||
│
|
|
||||||
│ uses
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────┐
|
|
||||||
│ Axios (HTTP Client) │
|
|
||||||
└──────────────┬──────────────────────────┘
|
|
||||||
│
|
|
||||||
│ requests
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────┐
|
|
||||||
│ Backend API Server │
|
|
||||||
└─────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Nguyên tắc sử dụng
|
|
||||||
|
|
||||||
### ✅ NÊN dùng Services khi:
|
|
||||||
- Gọi API từ non-React code (utilities, event handlers, etc.)
|
|
||||||
- Cần gọi API một lần rồi không cần tracking state
|
|
||||||
- Không cần caching hay background refetch
|
|
||||||
- Viết code không phụ thuộc React
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ✅ OK - utility function
|
|
||||||
export async function syncData() {
|
|
||||||
const software = await appVersionService.getSoftwareList();
|
|
||||||
const commands = await commandService.getCommandList();
|
|
||||||
return { software, commands };
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### ✅ NÊN dùng Hooks khi:
|
|
||||||
- Lấy/update dữ liệu trong React components
|
|
||||||
- Cần caching và background refetch
|
|
||||||
- Muốn dữ liệu tự động update
|
|
||||||
- Cần tracking loading/error states
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ✅ OK - React component
|
|
||||||
function Dashboard() {
|
|
||||||
const { data: software, isLoading } = useGetSoftwareList();
|
|
||||||
const uploadMutation = useUploadSoftware();
|
|
||||||
|
|
||||||
return <div>{/* ... */}</div>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### ❌ KHÔNG nên dùng Services khi:
|
|
||||||
- Đang trong React component và cần state management
|
|
||||||
- Cần automatic refetching
|
|
||||||
- Cần auto-invalidation sau mutations
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ❌ WRONG
|
|
||||||
function Component() {
|
|
||||||
const [data, setData] = useState([]);
|
|
||||||
useEffect(() => {
|
|
||||||
appVersionService.getSoftwareList().then(setData);
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ RIGHT
|
|
||||||
function Component() {
|
|
||||||
const { data } = useGetSoftwareList();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### ❌ KHÔNG nên dùng Hooks khi:
|
|
||||||
- Không phải trong React component
|
|
||||||
- Không có React context
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ❌ WRONG - không thể gọi hooks ở đây
|
|
||||||
export function initApp() {
|
|
||||||
const { data } = useGetSoftwareList(); // ERROR!
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ RIGHT
|
|
||||||
export async function initApp() {
|
|
||||||
const data = await appVersionService.getSoftwareList();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
**Services** = Cơ sở API calls, có thể tái sử dụng ở bất kỳ đâu
|
|
||||||
**Hooks** = Lớp React trên services, tối ưu cho React components
|
|
||||||
|
|
||||||
**Dùng Services** khi bạn cần tính linh hoạt và độc lập với React
|
|
||||||
**Dùng Hooks** khi bạn muốn TanStack Query quản lý state và caching tự động
|
|
||||||
|
|
@ -1,249 +0,0 @@
|
||||||
# System Admin Priority Logic - Hướng dẫn
|
|
||||||
|
|
||||||
## Tổng quan
|
|
||||||
|
|
||||||
Đã cập nhật logic để **System Admin** (Priority = 0) trở thành quyền cao nhất trong hệ thống.
|
|
||||||
|
|
||||||
### Quy tắc Priority
|
|
||||||
|
|
||||||
```
|
|
||||||
Priority càng thấp = Quyền càng cao
|
|
||||||
Priority = 0 (System Admin) = Quyền cao nhất
|
|
||||||
```
|
|
||||||
|
|
||||||
## Các thay đổi đã thực hiện
|
|
||||||
|
|
||||||
### 1. Constants mới (`src/config/constants.ts`)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export const SYSTEM_ADMIN_PRIORITY = 0;
|
|
||||||
|
|
||||||
export const RolePriority = {
|
|
||||||
SYSTEM_ADMIN: 0,
|
|
||||||
} as const;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Mục đích**: Định nghĩa giá trị priority của System Admin, tránh hardcode số 0 trong code.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. Helper Functions (`src/helpers/roleHelpers.ts`)
|
|
||||||
|
|
||||||
#### `isSystemAdminPriority(priority: number): boolean`
|
|
||||||
Kiểm tra xem priority có phải là System Admin không.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { isSystemAdminPriority } from '@/helpers/roleHelpers';
|
|
||||||
|
|
||||||
if (isSystemAdminPriority(userPriority)) {
|
|
||||||
// User là System Admin
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `hasHigherOrEqualPriority(priority1, priority2): boolean`
|
|
||||||
So sánh 2 priority (nhỏ hơn = cao hơn).
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
if (hasHigherOrEqualPriority(userPriority, requiredPriority)) {
|
|
||||||
// User có đủ quyền
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `getPriorityLabel(priority: number): string`
|
|
||||||
Lấy nhãn mô tả cho priority.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
getPriorityLabel(0) // "System Admin (Highest)"
|
|
||||||
getPriorityLabel(5) // "Priority 5"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. useAuth Hook (`src/hooks/useAuth.tsx`)
|
|
||||||
|
|
||||||
Thêm method mới: `isSystemAdmin()`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const { isSystemAdmin } = useAuth();
|
|
||||||
|
|
||||||
if (isSystemAdmin()) {
|
|
||||||
// User là System Admin (priority = 0)
|
|
||||||
console.log('You have highest permission!');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Interface cập nhật:**
|
|
||||||
```typescript
|
|
||||||
export interface IAuthContext {
|
|
||||||
// ... các field cũ
|
|
||||||
isSystemAdmin: () => boolean; // ← Mới
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. Sidebar Logic (`src/components/sidebars/app-sidebar.tsx`)
|
|
||||||
|
|
||||||
Cập nhật logic kiểm tra admin:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// TRƯỚC
|
|
||||||
const isAdmin = acs.includes(PermissionEnum.ALLOW_ALL);
|
|
||||||
|
|
||||||
// SAU
|
|
||||||
const isAdmin = acs.includes(PermissionEnum.ALLOW_ALL) || isSystemAdmin();
|
|
||||||
```
|
|
||||||
|
|
||||||
**Lợi ích:**
|
|
||||||
- System Admin (Priority = 0) thấy tất cả menu items
|
|
||||||
- Không cần phải có permission `ALLOW_ALL` trong database
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cách sử dụng
|
|
||||||
|
|
||||||
### Kiểm tra System Admin trong component
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
|
||||||
|
|
||||||
function MyComponent() {
|
|
||||||
const { isSystemAdmin, role } = useAuth();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{isSystemAdmin() && (
|
|
||||||
<AdminOnlyFeature />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<p>Role: {role.roleName}</p>
|
|
||||||
<p>Priority: {role.priority}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Kiểm tra priority trong logic nghiệp vụ
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { isSystemAdminPriority, hasHigherOrEqualPriority } from '@/helpers/roleHelpers';
|
|
||||||
|
|
||||||
function canDeleteUser(currentUserPriority: number, targetUserPriority: number): boolean {
|
|
||||||
// System Admin có thể xóa bất kỳ ai
|
|
||||||
if (isSystemAdminPriority(currentUserPriority)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// User chỉ có thể xóa user có priority thấp hơn (số lớn hơn)
|
|
||||||
return hasHigherOrEqualPriority(currentUserPriority, targetUserPriority);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Hiển thị label priority
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { getPriorityLabel } from '@/helpers/roleHelpers';
|
|
||||||
|
|
||||||
<Badge>{getPriorityLabel(role.priority)}</Badge>
|
|
||||||
// System Admin sẽ hiển thị: "System Admin (Highest)"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Luồng kiểm tra quyền
|
|
||||||
|
|
||||||
```
|
|
||||||
User đăng nhập
|
|
||||||
↓
|
|
||||||
Priority được lưu vào localStorage
|
|
||||||
↓
|
|
||||||
useAuth hook load priority
|
|
||||||
↓
|
|
||||||
isSystemAdmin() kiểm tra priority === 0
|
|
||||||
↓
|
|
||||||
Sidebar check: ALLOW_ALL || isSystemAdmin()
|
|
||||||
↓
|
|
||||||
Hiển thị menu items phù hợp
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Ví dụ thực tế
|
|
||||||
|
|
||||||
### Ví dụ 1: Ẩn/hiện nút Delete dựa trên priority
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
function UserManagement() {
|
|
||||||
const { role, isSystemAdmin } = useAuth();
|
|
||||||
const currentUserPriority = role.priority;
|
|
||||||
|
|
||||||
function canDelete(targetUserPriority: number): boolean {
|
|
||||||
// System Admin xóa được tất cả
|
|
||||||
if (isSystemAdmin()) return true;
|
|
||||||
|
|
||||||
// Priority thấp hơn (số nhỏ hơn) mới xóa được
|
|
||||||
return currentUserPriority < targetUserPriority;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table>
|
|
||||||
{users.map(user => (
|
|
||||||
<TableRow key={user.id}>
|
|
||||||
<TableCell>{user.name}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{canDelete(user.role.priority) && (
|
|
||||||
<DeleteButton userId={user.id} />
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</Table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ví dụ 2: Route protection
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
|
||||||
import { redirect } from '@tanstack/react-router';
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/_auth/admin-panel')({
|
|
||||||
beforeLoad: ({ context }) => {
|
|
||||||
const { isSystemAdmin } = context.auth;
|
|
||||||
|
|
||||||
if (!isSystemAdmin()) {
|
|
||||||
throw redirect({
|
|
||||||
to: '/unauthorized',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
component: AdminPanel,
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tóm tắt
|
|
||||||
|
|
||||||
✅ **Priority = 0** là System Admin (quyền cao nhất)
|
|
||||||
✅ **Priority thấp hơn** = Quyền cao hơn
|
|
||||||
✅ Có constants và helpers để tái sử dụng
|
|
||||||
✅ `isSystemAdmin()` method trong useAuth hook
|
|
||||||
✅ Sidebar tự động nhận biết System Admin
|
|
||||||
✅ Không cần hardcode giá trị priority nữa
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Files đã thay đổi
|
|
||||||
|
|
||||||
1. ✅ `src/config/constants.ts` - Constants mới
|
|
||||||
2. ✅ `src/helpers/roleHelpers.ts` - Helper functions
|
|
||||||
3. ✅ `src/hooks/useAuth.tsx` - Thêm isSystemAdmin()
|
|
||||||
4. ✅ `src/types/auth.ts` - Cập nhật interface
|
|
||||||
5. ✅ `src/components/sidebars/app-sidebar.tsx` - Logic admin check
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Lưu ý quan trọng:**
|
|
||||||
Backend cũng cần implement logic tương tự để đảm bảo consistency giữa frontend và backend!
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Debug Permissions</title>
|
|
||||||
<script>
|
|
||||||
// Paste this in browser console to debug
|
|
||||||
const acs = localStorage.getItem('acs');
|
|
||||||
const role = localStorage.getItem('role');
|
|
||||||
console.log('Current Role:', role);
|
|
||||||
console.log('Current ACS (raw):', acs);
|
|
||||||
console.log('Current ACS (parsed):', acs ? acs.split(',').map(Number) : []);
|
|
||||||
console.log('VIEW_AGENT permission code:', 171);
|
|
||||||
console.log('Has VIEW_AGENT?', acs ? acs.split(',').map(Number).includes(171) : false);
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Open Browser Console (F12)</h1>
|
|
||||||
<p>Or run this in console:</p>
|
|
||||||
<pre>
|
|
||||||
const acs = localStorage.getItem('acs');
|
|
||||||
console.log('Your permissions:', acs);
|
|
||||||
console.log('As array:', acs ? acs.split(',').map(Number) : []);
|
|
||||||
console.log('Has VIEW_AGENT (171)?', acs ? acs.split(',').includes('171') : false);
|
|
||||||
</pre>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -14,8 +14,7 @@ import {
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { useGetCommandsByTypes } from "@/hooks/queries/useCommandQueries";
|
import { useGetSensitiveCommands, useExecuteSensitiveCommand } from "@/hooks/queries/useCommandQueries";
|
||||||
import { useSendCommand } from "@/hooks/queries";
|
|
||||||
import { CommandType } from "@/types/command-registry";
|
import { CommandType } from "@/types/command-registry";
|
||||||
import {
|
import {
|
||||||
Power,
|
Power,
|
||||||
|
|
@ -58,6 +57,12 @@ const COMMAND_TYPE_CONFIG = {
|
||||||
color: "text-purple-600",
|
color: "text-purple-600",
|
||||||
bgColor: "bg-purple-50 hover:bg-purple-100",
|
bgColor: "bg-purple-50 hover:bg-purple-100",
|
||||||
},
|
},
|
||||||
|
[CommandType.RESET]: {
|
||||||
|
label : "Reset",
|
||||||
|
icon: Loader2,
|
||||||
|
color: "text-green-600",
|
||||||
|
bgColor: "bg-green-50 hover:bg-green-100",
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CommandActionButtons({ roomName, selectedDevices = [] }: CommandActionButtonsProps) {
|
export function CommandActionButtons({ roomName, selectedDevices = [] }: CommandActionButtonsProps) {
|
||||||
|
|
@ -65,55 +70,52 @@ export function CommandActionButtons({ roomName, selectedDevices = [] }: Command
|
||||||
open: boolean;
|
open: boolean;
|
||||||
command: any;
|
command: any;
|
||||||
commandType: CommandType;
|
commandType: CommandType;
|
||||||
|
isSensitive?: boolean;
|
||||||
}>({
|
}>({
|
||||||
open: false,
|
open: false,
|
||||||
command: null,
|
command: null,
|
||||||
commandType: CommandType.RESTART,
|
commandType: CommandType.RESTART,
|
||||||
});
|
});
|
||||||
const [isExecuting, setIsExecuting] = useState(false);
|
const [isExecuting, setIsExecuting] = useState(false);
|
||||||
|
const [sensitivePassword, setSensitivePassword] = useState("");
|
||||||
|
|
||||||
// Query commands for each type
|
// Query commands for each type
|
||||||
const { data: restartCommands = [] } = useGetCommandsByTypes(CommandType.RESTART.toString());
|
const { data: sensitiveCommands = [] } = useGetSensitiveCommands();
|
||||||
const { data: shutdownCommands = [] } = useGetCommandsByTypes(CommandType.SHUTDOWN.toString());
|
|
||||||
const { data: taskkillCommands = [] } = useGetCommandsByTypes(CommandType.TASKKILL.toString());
|
|
||||||
const { data: blockCommands = [] } = useGetCommandsByTypes(CommandType.BLOCK.toString());
|
|
||||||
|
|
||||||
// Send command mutation
|
// Send command mutation (sensitive)
|
||||||
const sendCommandMutation = useSendCommand();
|
const executeSensitiveMutation = useExecuteSensitiveCommand();
|
||||||
|
|
||||||
const commandsByType = {
|
// Build commands mapped by CommandType using the `command` field from sensitive data
|
||||||
[CommandType.RESTART]: restartCommands,
|
const commandsByType: Record<number, any[]> = (Object.values(CommandType) as Array<number | string>)
|
||||||
[CommandType.SHUTDOWN]: shutdownCommands,
|
.filter((v) => typeof v === "number")
|
||||||
[CommandType.TASKKILL]: taskkillCommands,
|
.reduce((acc: Record<number, any[]>, type) => {
|
||||||
[CommandType.BLOCK]: blockCommands,
|
acc[type as number] = (sensitiveCommands || []).filter((c: any) => Number(c.command) === Number(type));
|
||||||
};
|
return acc;
|
||||||
|
}, {} as Record<number, any[]>);
|
||||||
|
|
||||||
const handleCommandClick = (command: any, commandType: CommandType) => {
|
const handleCommandClick = (command: any, commandType: CommandType) => {
|
||||||
|
// When building from sensitiveCommands, all items here are sensitive
|
||||||
setConfirmDialog({
|
setConfirmDialog({
|
||||||
open: true,
|
open: true,
|
||||||
command,
|
command,
|
||||||
commandType,
|
commandType,
|
||||||
|
isSensitive: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirmExecute = async () => {
|
const handleConfirmExecute = async () => {
|
||||||
setIsExecuting(true);
|
setIsExecuting(true);
|
||||||
try {
|
try {
|
||||||
// Chuẩn bị data theo format API (PascalCase)
|
// All rendered commands are sourced from sensitiveCommands — send via sensitive mutation
|
||||||
const apiData = {
|
await executeSensitiveMutation.mutateAsync({
|
||||||
Command: confirmDialog.command.commandContent,
|
|
||||||
QoS: confirmDialog.command.qoS,
|
|
||||||
IsRetained: confirmDialog.command.isRetained,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Gửi lệnh đến phòng
|
|
||||||
await sendCommandMutation.mutateAsync({
|
|
||||||
roomName,
|
roomName,
|
||||||
data: apiData as any,
|
command: confirmDialog.command.commandContent,
|
||||||
|
password: sensitivePassword,
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.success(`Đã gửi lệnh: ${confirmDialog.command.commandName}`);
|
toast.success(`Đã gửi lệnh: ${confirmDialog.command.commandName}`);
|
||||||
setConfirmDialog({ open: false, command: null, commandType: CommandType.RESTART });
|
setConfirmDialog({ open: false, command: null, commandType: CommandType.RESTART, isSensitive: false });
|
||||||
|
setSensitivePassword("");
|
||||||
|
|
||||||
// Reload page để tránh freeze
|
// Reload page để tránh freeze
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -128,7 +130,8 @@ export function CommandActionButtons({ roomName, selectedDevices = [] }: Command
|
||||||
|
|
||||||
const handleCloseDialog = () => {
|
const handleCloseDialog = () => {
|
||||||
if (!isExecuting) {
|
if (!isExecuting) {
|
||||||
setConfirmDialog({ open: false, command: null, commandType: CommandType.RESTART });
|
setConfirmDialog({ open: false, command: null, commandType: CommandType.RESTART, isSensitive: false });
|
||||||
|
setSensitivePassword("");
|
||||||
// Reload để tránh freeze
|
// Reload để tránh freeze
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
|
|
@ -148,7 +151,7 @@ export function CommandActionButtons({ roomName, selectedDevices = [] }: Command
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled
|
disabled
|
||||||
size="sm"
|
size="sm"
|
||||||
className="gap-2"
|
className="gap-2 flex-shrink-0"
|
||||||
>
|
>
|
||||||
<Icon className={`h-4 w-4 ${config.color}`} />
|
<Icon className={`h-4 w-4 ${config.color}`} />
|
||||||
{config.label}
|
{config.label}
|
||||||
|
|
@ -163,7 +166,7 @@ export function CommandActionButtons({ roomName, selectedDevices = [] }: Command
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className={`gap-2 ${config.bgColor} border-${config.color.split('-')[1]}-200`}
|
className={`gap-2 ${config.bgColor} border-${config.color.split('-')[1]}-200 flex-shrink-0`}
|
||||||
>
|
>
|
||||||
<Icon className={`h-4 w-4 ${config.color}`} />
|
<Icon className={`h-4 w-4 ${config.color}`} />
|
||||||
{config.label}
|
{config.label}
|
||||||
|
|
@ -202,7 +205,7 @@ export function CommandActionButtons({ roomName, selectedDevices = [] }: Command
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex gap-2 flex-nowrap overflow-x-auto items-center whitespace-nowrap">
|
||||||
{Object.values(CommandType)
|
{Object.values(CommandType)
|
||||||
.filter((value) => typeof value === "number")
|
.filter((value) => typeof value === "number")
|
||||||
.map((commandType) => renderCommandButton(commandType as CommandType))}
|
.map((commandType) => renderCommandButton(commandType as CommandType))}
|
||||||
|
|
@ -225,6 +228,18 @@ export function CommandActionButtons({ roomName, selectedDevices = [] }: Command
|
||||||
{confirmDialog.command.description}
|
{confirmDialog.command.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{confirmDialog.isSensitive && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<label className="block text-sm font-medium mb-1">Mật khẩu</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={sensitivePassword}
|
||||||
|
onChange={(e) => setSensitivePassword(e.target.value)}
|
||||||
|
className="w-full px-2 py-1 rounded border"
|
||||||
|
placeholder="Nhập mật khẩu để xác nhận"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="bg-muted p-3 rounded-md space-y-1">
|
<div className="bg-muted p-3 rounded-md space-y-1">
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
<span className="font-medium">Phòng:</span> {roomName}
|
<span className="font-medium">Phòng:</span> {roomName}
|
||||||
|
|
@ -255,7 +270,7 @@ export function CommandActionButtons({ roomName, selectedDevices = [] }: Command
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleConfirmExecute}
|
onClick={handleConfirmExecute}
|
||||||
disabled={isExecuting}
|
disabled={isExecuting || (confirmDialog.isSensitive && !sensitivePassword)}
|
||||||
className="gap-2"
|
className="gap-2"
|
||||||
>
|
>
|
||||||
{isExecuting ? (
|
{isExecuting ? (
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Monitor, Wifi, WifiOff } from "lucide-react";
|
import { Monitor, Wifi, WifiOff, Loader2 } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { FolderStatusPopover } from "../folder-status-popover";
|
import { FolderStatusPopover } from "../folder-status-popover";
|
||||||
import type { ClientFolderStatus } from "@/hooks/useClientFolderStatus";
|
import { useGetClientFolderStatusForDevice } from "@/hooks/queries";
|
||||||
|
import type { ClientFolderStatus } from "@/types/folder";
|
||||||
export function ComputerCard({
|
export function ComputerCard({
|
||||||
device,
|
device,
|
||||||
position,
|
position,
|
||||||
|
|
@ -30,6 +31,63 @@ export function ComputerCard({
|
||||||
|
|
||||||
const isOffline = device.isOffline;
|
const isOffline = device.isOffline;
|
||||||
const firstNetworkInfo = device.networkInfos?.[0];
|
const firstNetworkInfo = device.networkInfos?.[0];
|
||||||
|
const agentVersion = device.version;
|
||||||
|
|
||||||
|
function DeviceFolderCheck() {
|
||||||
|
const deviceId = device.id;
|
||||||
|
const room = device.room;
|
||||||
|
const [checking, setChecking] = useState(false);
|
||||||
|
|
||||||
|
const { data: status, isLoading } = useGetClientFolderStatusForDevice(
|
||||||
|
deviceId,
|
||||||
|
room,
|
||||||
|
checking
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCheck = () => setChecking((s) => !s);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={handleCheck}
|
||||||
|
className="inline-flex items-center gap-2 px-3 py-1 rounded border bg-background text-sm"
|
||||||
|
>
|
||||||
|
{isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
|
||||||
|
Kiểm tra thư mục Setup
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{checking && isLoading && (
|
||||||
|
<div className="text-xs text-muted-foreground mt-2">Đang kiểm tra...</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{checking && !isLoading && status && (
|
||||||
|
<div className="text-xs mt-2">
|
||||||
|
<div className="font-medium">Các file trong thư mục Setup({status.currentFiles?.length ?? 0})</div>
|
||||||
|
<div className="mt-1 max-h-36 overflow-auto space-y-1">
|
||||||
|
{(status.currentFiles ?? []).length === 0 ? (
|
||||||
|
<div className="text-muted-foreground">Không có file hiện tại</div>
|
||||||
|
) : (
|
||||||
|
(status.currentFiles ?? []).map((f: any) => (
|
||||||
|
<div key={f.fileName} className="font-mono text-xs">
|
||||||
|
<div className="truncate">{f.fileName}</div>
|
||||||
|
{f.lastModified && (
|
||||||
|
<div className="text-muted-foreground text-[10px]">
|
||||||
|
{new Date(f.lastModified).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{checking && !isLoading && !status && (
|
||||||
|
<div className="text-xs text-muted-foreground mt-2">Không có dữ liệu</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const DeviceInfo = () => (
|
const DeviceInfo = () => (
|
||||||
<div className="space-y-3 min-w-[280px]">
|
<div className="space-y-3 min-w-[280px]">
|
||||||
|
|
@ -69,6 +127,11 @@ export function ComputerCard({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-muted-foreground mb-1">Kiểm tra thư mục</div>
|
||||||
|
<DeviceFolderCheck />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-muted-foreground mb-1">Trạng thái</div>
|
<div className="text-xs text-muted-foreground mb-1">Trạng thái</div>
|
||||||
<Badge
|
<Badge
|
||||||
|
|
@ -119,14 +182,14 @@ export function ComputerCard({
|
||||||
{firstNetworkInfo?.ipAddress && (
|
{firstNetworkInfo?.ipAddress && (
|
||||||
<div className="text-[10px] font-mono text-center mb-1 px-1 truncate w-full">
|
<div className="text-[10px] font-mono text-center mb-1 px-1 truncate w-full">
|
||||||
{firstNetworkInfo.ipAddress}
|
{firstNetworkInfo.ipAddress}
|
||||||
|
{agentVersion && (
|
||||||
|
<div className="text-[10px] font-mono text-center mb-1 px-1 truncate w-full">
|
||||||
|
v{agentVersion}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{isOffline ? (
|
|
||||||
<WifiOff className="h-3 w-3 text-red-600" />
|
|
||||||
) : (
|
|
||||||
<Wifi className="h-3 w-3 text-green-600" />
|
|
||||||
)}
|
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-xs font-medium",
|
"text-xs font-medium",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { CheckCircle2, AlertCircle, Loader2, AlertTriangle } from "lucide-react";
|
import { CheckCircle2, AlertCircle, Loader2, AlertTriangle } from "lucide-react";
|
||||||
import type { ClientFolderStatus } from "@/hooks/useClientFolderStatus";
|
import type { ClientFolderStatus } from "@/types/folder";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
|
|
@ -15,8 +15,10 @@ export function FolderStatusPopover({
|
||||||
status,
|
status,
|
||||||
isLoading,
|
isLoading,
|
||||||
}: FolderStatusPopoverProps) {
|
}: FolderStatusPopoverProps) {
|
||||||
const hasMissing = status && status.missingFiles.length > 0;
|
const missing = status?.missingFiles ?? [];
|
||||||
const hasExtra = status && status.extraFiles.length > 0;
|
const extra = status?.extraFiles ?? [];
|
||||||
|
const hasMissing = missing.length > 0;
|
||||||
|
const hasExtra = extra.length > 0;
|
||||||
const hasIssues = hasMissing || hasExtra;
|
const hasIssues = hasMissing || hasExtra;
|
||||||
|
|
||||||
// Xác định màu sắc và icon dựa trên trạng thái
|
// Xác định màu sắc và icon dựa trên trạng thái
|
||||||
|
|
@ -80,11 +82,11 @@ export function FolderStatusPopover({
|
||||||
<div className="border-l-4 border-red-500 pl-3">
|
<div className="border-l-4 border-red-500 pl-3">
|
||||||
<h4 className="text-sm font-semibold text-red-600 mb-2 flex items-center gap-2">
|
<h4 className="text-sm font-semibold text-red-600 mb-2 flex items-center gap-2">
|
||||||
<AlertCircle className="h-4 w-4" />
|
<AlertCircle className="h-4 w-4" />
|
||||||
File thiếu ({status.missingFiles.length})
|
File thiếu ({missing.length})
|
||||||
</h4>
|
</h4>
|
||||||
<ScrollArea className="h-32 rounded-md border bg-red-50/30 p-2">
|
<ScrollArea className="h-32 rounded-md border bg-red-50/30 p-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{status.missingFiles.map((file, idx) => (
|
{missing.map((file, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
className="text-xs bg-white rounded p-2 border border-red-200"
|
className="text-xs bg-white rounded p-2 border border-red-200"
|
||||||
|
|
@ -107,11 +109,11 @@ export function FolderStatusPopover({
|
||||||
<div className="border-l-4 border-orange-500 pl-3">
|
<div className="border-l-4 border-orange-500 pl-3">
|
||||||
<h4 className="text-sm font-semibold text-orange-600 mb-2 flex items-center gap-2">
|
<h4 className="text-sm font-semibold text-orange-600 mb-2 flex items-center gap-2">
|
||||||
<AlertCircle className="h-4 w-4" />
|
<AlertCircle className="h-4 w-4" />
|
||||||
File thừa ({status.extraFiles.length})
|
File thừa ({extra.length})
|
||||||
</h4>
|
</h4>
|
||||||
<ScrollArea className="h-32 rounded-md border bg-orange-50/30 p-2">
|
<ScrollArea className="h-32 rounded-md border bg-orange-50/30 p-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{status.extraFiles.map((file, idx) => (
|
{extra.map((file, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
className="text-xs bg-white rounded p-2 border border-orange-200"
|
className="text-xs bg-white rounded p-2 border border-orange-200"
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ export function CommandRegistryForm({
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full space-y-6">
|
<div className="w-full max-w-[90vw] sm:max-w-[70vw] md:max-w-[50vw] mx-auto space-y-6">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{title}</CardTitle>
|
<CardTitle>{title}</CardTitle>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Monitor, DoorOpen } from "lucide-react";
|
import { Monitor, DoorOpen } from "lucide-react";
|
||||||
import { ComputerCard } from "../cards/computer-card";
|
import { ComputerCard } from "../cards/computer-card";
|
||||||
import { useMachineNumber } from "../../hooks/useMachineNumber";
|
import { useMachineNumber } from "../../hooks/useMachineNumber";
|
||||||
import type { ClientFolderStatus } from "@/hooks/useClientFolderStatus";
|
import type { ClientFolderStatus } from "@/types/folder";
|
||||||
|
|
||||||
export function DeviceGrid({
|
export function DeviceGrid({
|
||||||
devices,
|
devices,
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,9 @@ import { Button } from "@/components/ui/button";
|
||||||
import { Wifi, WifiOff, Clock, MapPin, Monitor, ChevronLeft, ChevronRight } from "lucide-react";
|
import { Wifi, WifiOff, Clock, MapPin, Monitor, ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
import { useMachineNumber } from "@/hooks/useMachineNumber";
|
import { useMachineNumber } from "@/hooks/useMachineNumber";
|
||||||
import { FolderStatusPopover } from "../folder-status-popover";
|
import { FolderStatusPopover } from "../folder-status-popover";
|
||||||
import type { ClientFolderStatus } from "@/hooks/useClientFolderStatus";
|
|
||||||
|
|
||||||
interface DeviceTableProps {
|
interface DeviceTableProps {
|
||||||
devices: any[];
|
devices: any[];
|
||||||
folderStatuses?: Map<string, ClientFolderStatus>;
|
|
||||||
isCheckingFolder?: boolean;
|
isCheckingFolder?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,7 +30,6 @@ interface DeviceTableProps {
|
||||||
*/
|
*/
|
||||||
export function DeviceTable({
|
export function DeviceTable({
|
||||||
devices,
|
devices,
|
||||||
folderStatuses,
|
|
||||||
isCheckingFolder,
|
isCheckingFolder,
|
||||||
}: DeviceTableProps) {
|
}: DeviceTableProps) {
|
||||||
const getMachineNumber = useMachineNumber();
|
const getMachineNumber = useMachineNumber();
|
||||||
|
|
@ -151,7 +148,6 @@ export function DeviceTable({
|
||||||
const device = row.original;
|
const device = row.original;
|
||||||
const isOffline = device.isOffline;
|
const isOffline = device.isOffline;
|
||||||
const macAddress = device.networkInfos?.[0]?.macAddress || device.id;
|
const macAddress = device.networkInfos?.[0]?.macAddress || device.id;
|
||||||
const folderStatus = folderStatuses?.get(macAddress);
|
|
||||||
|
|
||||||
if (isOffline) {
|
if (isOffline) {
|
||||||
return <span className="text-muted-foreground text-sm">-</span>;
|
return <span className="text-muted-foreground text-sm">-</span>;
|
||||||
|
|
@ -160,7 +156,6 @@ export function DeviceTable({
|
||||||
return (
|
return (
|
||||||
<FolderStatusPopover
|
<FolderStatusPopover
|
||||||
deviceId={macAddress}
|
deviceId={macAddress}
|
||||||
status={folderStatus}
|
|
||||||
isLoading={isCheckingFolder}
|
isLoading={isCheckingFolder}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ export const API_ENDPOINTS = {
|
||||||
PING: `${BASE_URL}/ping`,
|
PING: `${BASE_URL}/ping`,
|
||||||
CSRF_TOKEN: `${BASE_URL}/csrf-token`,
|
CSRF_TOKEN: `${BASE_URL}/csrf-token`,
|
||||||
CREATE_ACCOUNT: `${BASE_URL}/auth/create-account`,
|
CREATE_ACCOUNT: `${BASE_URL}/auth/create-account`,
|
||||||
|
GET_USERS_LIST: `${BASE_URL}/users-info`,
|
||||||
},
|
},
|
||||||
APP_VERSION: {
|
APP_VERSION: {
|
||||||
//agent and app api
|
//agent and app api
|
||||||
|
|
@ -45,7 +46,7 @@ export const API_ENDPOINTS = {
|
||||||
SEND_COMMAND: (roomName: string) => `${BASE_URL}/DeviceComm/shellcommand/${roomName}`,
|
SEND_COMMAND: (roomName: string) => `${BASE_URL}/DeviceComm/shellcommand/${roomName}`,
|
||||||
CHANGE_DEVICE_ROOM: `${BASE_URL}/DeviceComm/changeroom`,
|
CHANGE_DEVICE_ROOM: `${BASE_URL}/DeviceComm/changeroom`,
|
||||||
REQUEST_GET_CLIENT_FOLDER_STATUS: (roomName: string) =>
|
REQUEST_GET_CLIENT_FOLDER_STATUS: (roomName: string) =>
|
||||||
`${BASE_URL}/DeviceComm/clientfolderstatus/${roomName}`,
|
`${BASE_URL}/DeviceComm/folderstatuses/${roomName}`,
|
||||||
},
|
},
|
||||||
COMMAND: {
|
COMMAND: {
|
||||||
ADD_COMMAND: `${BASE_URL}/Command/add`,
|
ADD_COMMAND: `${BASE_URL}/Command/add`,
|
||||||
|
|
@ -53,6 +54,8 @@ export const API_ENDPOINTS = {
|
||||||
GET_COMMAND_BY_TYPES: (types: string) => `${BASE_URL}/Command/types/${types}`,
|
GET_COMMAND_BY_TYPES: (types: string) => `${BASE_URL}/Command/types/${types}`,
|
||||||
UPDATE_COMMAND: (commandId: number) => `${BASE_URL}/Command/update/${commandId}`,
|
UPDATE_COMMAND: (commandId: number) => `${BASE_URL}/Command/update/${commandId}`,
|
||||||
DELETE_COMMAND: (commandId: number) => `${BASE_URL}/Command/delete/${commandId}`,
|
DELETE_COMMAND: (commandId: number) => `${BASE_URL}/Command/delete/${commandId}`,
|
||||||
|
GET_SENSITIVE_COMMANDS: `${BASE_URL}/Command/sensitive`,
|
||||||
|
REQUEST_SEND_SENSITIVE_COMMAND: `${BASE_URL}/Command/send-sensitive`,
|
||||||
},
|
},
|
||||||
SSE_EVENTS: {
|
SSE_EVENTS: {
|
||||||
DEVICE_ONLINE: `${BASE_URL}/Sse/events/onlineDevices`,
|
DEVICE_ONLINE: `${BASE_URL}/Sse/events/onlineDevices`,
|
||||||
|
|
|
||||||
|
|
@ -15,3 +15,5 @@ export * from "./usePermissionQueries";
|
||||||
|
|
||||||
// Role Queries
|
// Role Queries
|
||||||
export * from "./useRoleQueries";
|
export * from "./useRoleQueries";
|
||||||
|
// User Queries
|
||||||
|
export * from "./useUserQueries";
|
||||||
|
|
|
||||||
|
|
@ -82,3 +82,47 @@ export function useDeleteCommand() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook để lấy danh sách lệnh nhạy cảm
|
||||||
|
*/
|
||||||
|
export function useGetSensitiveCommands(enabled = true) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [...COMMAND_QUERY_KEYS.all, "sensitive"],
|
||||||
|
queryFn: () => commandService.getSensitiveCommands(),
|
||||||
|
enabled,
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook để gửi lệnh nhạy cảm
|
||||||
|
*/
|
||||||
|
export function useExecuteSensitiveCommand() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
roomName,
|
||||||
|
command,
|
||||||
|
password,
|
||||||
|
}: {
|
||||||
|
roomName: string;
|
||||||
|
command: any;
|
||||||
|
password: string;
|
||||||
|
}) =>
|
||||||
|
// API expects a SensitiveCommandRequest with PascalCase keys
|
||||||
|
commandService.requestSendSensitiveCommand({
|
||||||
|
Command: command,
|
||||||
|
Password: password,
|
||||||
|
RoomName: roomName,
|
||||||
|
}),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [...COMMAND_QUERY_KEYS.all, "sensitive"],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import * as deviceCommService from "@/services/device-comm.service";
|
import * as deviceCommService from "@/services/device-comm.service";
|
||||||
import type { DeviceHealthCheck } from "@/types/device";
|
import type { DeviceHealthCheck } from "@/types/device";
|
||||||
|
import type { ClientFolderStatus } from "@/types/folder";
|
||||||
|
|
||||||
const DEVICE_COMM_QUERY_KEYS = {
|
const DEVICE_COMM_QUERY_KEYS = {
|
||||||
all: ["device-comm"] as const,
|
all: ["device-comm"] as const,
|
||||||
|
|
@ -158,7 +159,7 @@ export function useChangeDeviceRoom() {
|
||||||
* Hook để lấy trạng thái folder client
|
* Hook để lấy trạng thái folder client
|
||||||
*/
|
*/
|
||||||
export function useGetClientFolderStatus(roomName?: string, enabled = true) {
|
export function useGetClientFolderStatus(roomName?: string, enabled = true) {
|
||||||
return useQuery({
|
return useQuery<ClientFolderStatus[]>({
|
||||||
queryKey: roomName
|
queryKey: roomName
|
||||||
? DEVICE_COMM_QUERY_KEYS.clientFolderStatus(roomName)
|
? DEVICE_COMM_QUERY_KEYS.clientFolderStatus(roomName)
|
||||||
: ["disabled"],
|
: ["disabled"],
|
||||||
|
|
@ -170,3 +171,28 @@ export function useGetClientFolderStatus(roomName?: string, enabled = true) {
|
||||||
staleTime: 30 * 1000,
|
staleTime: 30 * 1000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to get folder status for a single device. The hook will fetch the
|
||||||
|
* folder status list for the device's room and return the matching entry
|
||||||
|
* for the provided `deviceId`.
|
||||||
|
*/
|
||||||
|
export function useGetClientFolderStatusForDevice(
|
||||||
|
deviceId?: string,
|
||||||
|
roomName?: string,
|
||||||
|
enabled = true
|
||||||
|
) {
|
||||||
|
return useQuery<ClientFolderStatus | undefined>({
|
||||||
|
queryKey: deviceId
|
||||||
|
? [...DEVICE_COMM_QUERY_KEYS.all, "folder-status-device", deviceId]
|
||||||
|
: ["disabled"],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!roomName) return Promise.reject("No room");
|
||||||
|
const list = await deviceCommService.getClientFolderStatus(roomName);
|
||||||
|
if (!Array.isArray(list)) return undefined;
|
||||||
|
return list.find((s: ClientFolderStatus) => s.deviceId === deviceId);
|
||||||
|
},
|
||||||
|
enabled: enabled && !!deviceId && !!roomName,
|
||||||
|
staleTime: 30 * 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
20
src/hooks/queries/useUserQueries.ts
Normal file
20
src/hooks/queries/useUserQueries.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import * as userService from "@/services/user.service";
|
||||||
|
import type { UserProfile } from "@/types/user-profile";
|
||||||
|
|
||||||
|
const USER_QUERY_KEYS = {
|
||||||
|
all: ["users"] as const,
|
||||||
|
list: () => [...USER_QUERY_KEYS.all, "list"] as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook để lấy danh sách thông tin người dùng
|
||||||
|
*/
|
||||||
|
export function useGetUsersInfo(enabled = true) {
|
||||||
|
return useQuery<UserProfile[]>({
|
||||||
|
queryKey: USER_QUERY_KEYS.list(),
|
||||||
|
queryFn: () => userService.getUsersInfo(),
|
||||||
|
enabled,
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { API_ENDPOINTS } from "@/config/api";
|
|
||||||
|
|
||||||
export interface MissingFiles {
|
|
||||||
fileName: string;
|
|
||||||
folderPath: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ExtraFiles {
|
|
||||||
fileName: string;
|
|
||||||
folderPath: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClientFolderStatus {
|
|
||||||
id: number;
|
|
||||||
deviceId: string;
|
|
||||||
missingFiles: MissingFiles[];
|
|
||||||
extraFiles: ExtraFiles[];
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useClientFolderStatus(roomName?: string) {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const reconnectTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
||||||
const [folderStatuses, setFolderStatuses] = useState<
|
|
||||||
Map<string, ClientFolderStatus>
|
|
||||||
>(new Map());
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let eventSource: EventSource | null = null;
|
|
||||||
|
|
||||||
const connect = () => {
|
|
||||||
eventSource = new EventSource(
|
|
||||||
API_ENDPOINTS.SSE_EVENTS.GET_CLIENT_FOLDER_STATUS
|
|
||||||
);
|
|
||||||
|
|
||||||
eventSource.addEventListener("clientFolderStatus", (event) => {
|
|
||||||
try {
|
|
||||||
const data: ClientFolderStatus = JSON.parse(event.data);
|
|
||||||
|
|
||||||
if (roomName && data.deviceId) {
|
|
||||||
setFolderStatuses((prev) => {
|
|
||||||
const newMap = new Map(prev);
|
|
||||||
newMap.set(data.deviceId, data);
|
|
||||||
return newMap;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Also cache in React Query for persistence
|
|
||||||
queryClient.setQueryData(
|
|
||||||
["folderStatus", data.deviceId],
|
|
||||||
data
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error parsing clientFolderStatus event:", err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const onError = (err: any) => {
|
|
||||||
console.error("SSE connection error:", err);
|
|
||||||
cleanup();
|
|
||||||
reconnectTimeout.current = setTimeout(connect, 5000);
|
|
||||||
};
|
|
||||||
|
|
||||||
eventSource.onerror = onError;
|
|
||||||
};
|
|
||||||
|
|
||||||
const cleanup = () => {
|
|
||||||
if (eventSource) eventSource.close();
|
|
||||||
if (reconnectTimeout.current) {
|
|
||||||
clearTimeout(reconnectTimeout.current);
|
|
||||||
reconnectTimeout.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
connect();
|
|
||||||
return cleanup;
|
|
||||||
}, [roomName, queryClient]);
|
|
||||||
|
|
||||||
return folderStatuses;
|
|
||||||
}
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
import { Route as rootRouteImport } from './routes/__root'
|
import { Route as rootRouteImport } from './routes/__root'
|
||||||
import { Route as AuthRouteImport } from './routes/_auth'
|
import { Route as AuthRouteImport } from './routes/_auth'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
|
import { Route as AuthUserIndexRouteImport } from './routes/_auth/user/index'
|
||||||
import { Route as AuthRoomsIndexRouteImport } from './routes/_auth/rooms/index'
|
import { Route as AuthRoomsIndexRouteImport } from './routes/_auth/rooms/index'
|
||||||
import { Route as AuthRoleIndexRouteImport } from './routes/_auth/role/index'
|
import { Route as AuthRoleIndexRouteImport } from './routes/_auth/role/index'
|
||||||
import { Route as AuthDeviceIndexRouteImport } from './routes/_auth/device/index'
|
import { Route as AuthDeviceIndexRouteImport } from './routes/_auth/device/index'
|
||||||
|
|
@ -20,7 +21,6 @@ import { Route as AuthBlacklistsIndexRouteImport } from './routes/_auth/blacklis
|
||||||
import { Route as AuthAppsIndexRouteImport } from './routes/_auth/apps/index'
|
import { Route as AuthAppsIndexRouteImport } from './routes/_auth/apps/index'
|
||||||
import { Route as AuthAgentIndexRouteImport } from './routes/_auth/agent/index'
|
import { Route as AuthAgentIndexRouteImport } from './routes/_auth/agent/index'
|
||||||
import { Route as authLoginIndexRouteImport } from './routes/(auth)/login/index'
|
import { Route as authLoginIndexRouteImport } from './routes/(auth)/login/index'
|
||||||
import { Route as AuthUserRoleIndexRouteImport } from './routes/_auth/user/role/index'
|
|
||||||
import { Route as AuthUserCreateIndexRouteImport } from './routes/_auth/user/create/index'
|
import { Route as AuthUserCreateIndexRouteImport } from './routes/_auth/user/create/index'
|
||||||
import { Route as AuthRoomsRoomNameIndexRouteImport } from './routes/_auth/rooms/$roomName/index'
|
import { Route as AuthRoomsRoomNameIndexRouteImport } from './routes/_auth/rooms/$roomName/index'
|
||||||
import { Route as AuthRoleCreateIndexRouteImport } from './routes/_auth/role/create/index'
|
import { Route as AuthRoleCreateIndexRouteImport } from './routes/_auth/role/create/index'
|
||||||
|
|
@ -28,6 +28,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 AuthUserRoleRoleIdIndexRouteImport } from './routes/_auth/user/role/$roleId/index'
|
import { Route as AuthUserRoleRoleIdIndexRouteImport } from './routes/_auth/user/role/$roleId/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 AuthRoleIdEditIndexRouteImport } from './routes/_auth/role/$id/edit/index'
|
import { Route as AuthRoleIdEditIndexRouteImport } from './routes/_auth/role/$id/edit/index'
|
||||||
|
|
||||||
const AuthRoute = AuthRouteImport.update({
|
const AuthRoute = AuthRouteImport.update({
|
||||||
|
|
@ -39,6 +40,11 @@ const IndexRoute = IndexRouteImport.update({
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const AuthUserIndexRoute = AuthUserIndexRouteImport.update({
|
||||||
|
id: '/user/',
|
||||||
|
path: '/user/',
|
||||||
|
getParentRoute: () => AuthRoute,
|
||||||
|
} as any)
|
||||||
const AuthRoomsIndexRoute = AuthRoomsIndexRouteImport.update({
|
const AuthRoomsIndexRoute = AuthRoomsIndexRouteImport.update({
|
||||||
id: '/rooms/',
|
id: '/rooms/',
|
||||||
path: '/rooms/',
|
path: '/rooms/',
|
||||||
|
|
@ -84,11 +90,6 @@ const authLoginIndexRoute = authLoginIndexRouteImport.update({
|
||||||
path: '/login/',
|
path: '/login/',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
const AuthUserRoleIndexRoute = AuthUserRoleIndexRouteImport.update({
|
|
||||||
id: '/user/role/',
|
|
||||||
path: '/user/role/',
|
|
||||||
getParentRoute: () => AuthRoute,
|
|
||||||
} as any)
|
|
||||||
const AuthUserCreateIndexRoute = AuthUserCreateIndexRouteImport.update({
|
const AuthUserCreateIndexRoute = AuthUserCreateIndexRouteImport.update({
|
||||||
id: '/user/create/',
|
id: '/user/create/',
|
||||||
path: '/user/create/',
|
path: '/user/create/',
|
||||||
|
|
@ -127,6 +128,12 @@ const AuthUserChangePasswordUserNameIndexRoute =
|
||||||
path: '/user/change-password/$userName/',
|
path: '/user/change-password/$userName/',
|
||||||
getParentRoute: () => AuthRoute,
|
getParentRoute: () => AuthRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const AuthRoomsRoomNameFolderStatusIndexRoute =
|
||||||
|
AuthRoomsRoomNameFolderStatusIndexRouteImport.update({
|
||||||
|
id: '/rooms/$roomName/folder-status/',
|
||||||
|
path: '/rooms/$roomName/folder-status/',
|
||||||
|
getParentRoute: () => AuthRoute,
|
||||||
|
} as any)
|
||||||
const AuthRoleIdEditIndexRoute = AuthRoleIdEditIndexRouteImport.update({
|
const AuthRoleIdEditIndexRoute = AuthRoleIdEditIndexRouteImport.update({
|
||||||
id: '/role/$id/edit/',
|
id: '/role/$id/edit/',
|
||||||
path: '/role/$id/edit/',
|
path: '/role/$id/edit/',
|
||||||
|
|
@ -144,13 +151,14 @@ export interface FileRoutesByFullPath {
|
||||||
'/device': typeof AuthDeviceIndexRoute
|
'/device': typeof AuthDeviceIndexRoute
|
||||||
'/role': typeof AuthRoleIndexRoute
|
'/role': typeof AuthRoleIndexRoute
|
||||||
'/rooms': typeof AuthRoomsIndexRoute
|
'/rooms': typeof AuthRoomsIndexRoute
|
||||||
|
'/user': typeof AuthUserIndexRoute
|
||||||
'/profile/$userName': typeof AuthProfileUserNameIndexRoute
|
'/profile/$userName': typeof AuthProfileUserNameIndexRoute
|
||||||
'/profile/change-password': typeof AuthProfileChangePasswordIndexRoute
|
'/profile/change-password': typeof AuthProfileChangePasswordIndexRoute
|
||||||
'/role/create': typeof AuthRoleCreateIndexRoute
|
'/role/create': typeof AuthRoleCreateIndexRoute
|
||||||
'/rooms/$roomName': typeof AuthRoomsRoomNameIndexRoute
|
'/rooms/$roomName': typeof AuthRoomsRoomNameIndexRoute
|
||||||
'/user/create': typeof AuthUserCreateIndexRoute
|
'/user/create': typeof AuthUserCreateIndexRoute
|
||||||
'/user/role': typeof AuthUserRoleIndexRoute
|
|
||||||
'/role/$id/edit': typeof AuthRoleIdEditIndexRoute
|
'/role/$id/edit': typeof AuthRoleIdEditIndexRoute
|
||||||
|
'/rooms/$roomName/folder-status': typeof AuthRoomsRoomNameFolderStatusIndexRoute
|
||||||
'/user/change-password/$userName': typeof AuthUserChangePasswordUserNameIndexRoute
|
'/user/change-password/$userName': typeof AuthUserChangePasswordUserNameIndexRoute
|
||||||
'/user/role/$roleId': typeof AuthUserRoleRoleIdIndexRoute
|
'/user/role/$roleId': typeof AuthUserRoleRoleIdIndexRoute
|
||||||
}
|
}
|
||||||
|
|
@ -165,13 +173,14 @@ export interface FileRoutesByTo {
|
||||||
'/device': typeof AuthDeviceIndexRoute
|
'/device': typeof AuthDeviceIndexRoute
|
||||||
'/role': typeof AuthRoleIndexRoute
|
'/role': typeof AuthRoleIndexRoute
|
||||||
'/rooms': typeof AuthRoomsIndexRoute
|
'/rooms': typeof AuthRoomsIndexRoute
|
||||||
|
'/user': typeof AuthUserIndexRoute
|
||||||
'/profile/$userName': typeof AuthProfileUserNameIndexRoute
|
'/profile/$userName': typeof AuthProfileUserNameIndexRoute
|
||||||
'/profile/change-password': typeof AuthProfileChangePasswordIndexRoute
|
'/profile/change-password': typeof AuthProfileChangePasswordIndexRoute
|
||||||
'/role/create': typeof AuthRoleCreateIndexRoute
|
'/role/create': typeof AuthRoleCreateIndexRoute
|
||||||
'/rooms/$roomName': typeof AuthRoomsRoomNameIndexRoute
|
'/rooms/$roomName': typeof AuthRoomsRoomNameIndexRoute
|
||||||
'/user/create': typeof AuthUserCreateIndexRoute
|
'/user/create': typeof AuthUserCreateIndexRoute
|
||||||
'/user/role': typeof AuthUserRoleIndexRoute
|
|
||||||
'/role/$id/edit': typeof AuthRoleIdEditIndexRoute
|
'/role/$id/edit': typeof AuthRoleIdEditIndexRoute
|
||||||
|
'/rooms/$roomName/folder-status': typeof AuthRoomsRoomNameFolderStatusIndexRoute
|
||||||
'/user/change-password/$userName': typeof AuthUserChangePasswordUserNameIndexRoute
|
'/user/change-password/$userName': typeof AuthUserChangePasswordUserNameIndexRoute
|
||||||
'/user/role/$roleId': typeof AuthUserRoleRoleIdIndexRoute
|
'/user/role/$roleId': typeof AuthUserRoleRoleIdIndexRoute
|
||||||
}
|
}
|
||||||
|
|
@ -188,13 +197,14 @@ export interface FileRoutesById {
|
||||||
'/_auth/device/': typeof AuthDeviceIndexRoute
|
'/_auth/device/': typeof AuthDeviceIndexRoute
|
||||||
'/_auth/role/': typeof AuthRoleIndexRoute
|
'/_auth/role/': typeof AuthRoleIndexRoute
|
||||||
'/_auth/rooms/': typeof AuthRoomsIndexRoute
|
'/_auth/rooms/': typeof AuthRoomsIndexRoute
|
||||||
|
'/_auth/user/': typeof AuthUserIndexRoute
|
||||||
'/_auth/profile/$userName/': typeof AuthProfileUserNameIndexRoute
|
'/_auth/profile/$userName/': typeof AuthProfileUserNameIndexRoute
|
||||||
'/_auth/profile/change-password/': typeof AuthProfileChangePasswordIndexRoute
|
'/_auth/profile/change-password/': typeof AuthProfileChangePasswordIndexRoute
|
||||||
'/_auth/role/create/': typeof AuthRoleCreateIndexRoute
|
'/_auth/role/create/': typeof AuthRoleCreateIndexRoute
|
||||||
'/_auth/rooms/$roomName/': typeof AuthRoomsRoomNameIndexRoute
|
'/_auth/rooms/$roomName/': typeof AuthRoomsRoomNameIndexRoute
|
||||||
'/_auth/user/create/': typeof AuthUserCreateIndexRoute
|
'/_auth/user/create/': typeof AuthUserCreateIndexRoute
|
||||||
'/_auth/user/role/': typeof AuthUserRoleIndexRoute
|
|
||||||
'/_auth/role/$id/edit/': typeof AuthRoleIdEditIndexRoute
|
'/_auth/role/$id/edit/': typeof AuthRoleIdEditIndexRoute
|
||||||
|
'/_auth/rooms/$roomName/folder-status/': typeof AuthRoomsRoomNameFolderStatusIndexRoute
|
||||||
'/_auth/user/change-password/$userName/': typeof AuthUserChangePasswordUserNameIndexRoute
|
'/_auth/user/change-password/$userName/': typeof AuthUserChangePasswordUserNameIndexRoute
|
||||||
'/_auth/user/role/$roleId/': typeof AuthUserRoleRoleIdIndexRoute
|
'/_auth/user/role/$roleId/': typeof AuthUserRoleRoleIdIndexRoute
|
||||||
}
|
}
|
||||||
|
|
@ -211,13 +221,14 @@ export interface FileRouteTypes {
|
||||||
| '/device'
|
| '/device'
|
||||||
| '/role'
|
| '/role'
|
||||||
| '/rooms'
|
| '/rooms'
|
||||||
|
| '/user'
|
||||||
| '/profile/$userName'
|
| '/profile/$userName'
|
||||||
| '/profile/change-password'
|
| '/profile/change-password'
|
||||||
| '/role/create'
|
| '/role/create'
|
||||||
| '/rooms/$roomName'
|
| '/rooms/$roomName'
|
||||||
| '/user/create'
|
| '/user/create'
|
||||||
| '/user/role'
|
|
||||||
| '/role/$id/edit'
|
| '/role/$id/edit'
|
||||||
|
| '/rooms/$roomName/folder-status'
|
||||||
| '/user/change-password/$userName'
|
| '/user/change-password/$userName'
|
||||||
| '/user/role/$roleId'
|
| '/user/role/$roleId'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
|
|
@ -232,13 +243,14 @@ export interface FileRouteTypes {
|
||||||
| '/device'
|
| '/device'
|
||||||
| '/role'
|
| '/role'
|
||||||
| '/rooms'
|
| '/rooms'
|
||||||
|
| '/user'
|
||||||
| '/profile/$userName'
|
| '/profile/$userName'
|
||||||
| '/profile/change-password'
|
| '/profile/change-password'
|
||||||
| '/role/create'
|
| '/role/create'
|
||||||
| '/rooms/$roomName'
|
| '/rooms/$roomName'
|
||||||
| '/user/create'
|
| '/user/create'
|
||||||
| '/user/role'
|
|
||||||
| '/role/$id/edit'
|
| '/role/$id/edit'
|
||||||
|
| '/rooms/$roomName/folder-status'
|
||||||
| '/user/change-password/$userName'
|
| '/user/change-password/$userName'
|
||||||
| '/user/role/$roleId'
|
| '/user/role/$roleId'
|
||||||
id:
|
id:
|
||||||
|
|
@ -254,13 +266,14 @@ export interface FileRouteTypes {
|
||||||
| '/_auth/device/'
|
| '/_auth/device/'
|
||||||
| '/_auth/role/'
|
| '/_auth/role/'
|
||||||
| '/_auth/rooms/'
|
| '/_auth/rooms/'
|
||||||
|
| '/_auth/user/'
|
||||||
| '/_auth/profile/$userName/'
|
| '/_auth/profile/$userName/'
|
||||||
| '/_auth/profile/change-password/'
|
| '/_auth/profile/change-password/'
|
||||||
| '/_auth/role/create/'
|
| '/_auth/role/create/'
|
||||||
| '/_auth/rooms/$roomName/'
|
| '/_auth/rooms/$roomName/'
|
||||||
| '/_auth/user/create/'
|
| '/_auth/user/create/'
|
||||||
| '/_auth/user/role/'
|
|
||||||
| '/_auth/role/$id/edit/'
|
| '/_auth/role/$id/edit/'
|
||||||
|
| '/_auth/rooms/$roomName/folder-status/'
|
||||||
| '/_auth/user/change-password/$userName/'
|
| '/_auth/user/change-password/$userName/'
|
||||||
| '/_auth/user/role/$roleId/'
|
| '/_auth/user/role/$roleId/'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
|
|
@ -287,6 +300,13 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof IndexRouteImport
|
preLoaderRoute: typeof IndexRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/_auth/user/': {
|
||||||
|
id: '/_auth/user/'
|
||||||
|
path: '/user'
|
||||||
|
fullPath: '/user'
|
||||||
|
preLoaderRoute: typeof AuthUserIndexRouteImport
|
||||||
|
parentRoute: typeof AuthRoute
|
||||||
|
}
|
||||||
'/_auth/rooms/': {
|
'/_auth/rooms/': {
|
||||||
id: '/_auth/rooms/'
|
id: '/_auth/rooms/'
|
||||||
path: '/rooms'
|
path: '/rooms'
|
||||||
|
|
@ -350,13 +370,6 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof authLoginIndexRouteImport
|
preLoaderRoute: typeof authLoginIndexRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
'/_auth/user/role/': {
|
|
||||||
id: '/_auth/user/role/'
|
|
||||||
path: '/user/role'
|
|
||||||
fullPath: '/user/role'
|
|
||||||
preLoaderRoute: typeof AuthUserRoleIndexRouteImport
|
|
||||||
parentRoute: typeof AuthRoute
|
|
||||||
}
|
|
||||||
'/_auth/user/create/': {
|
'/_auth/user/create/': {
|
||||||
id: '/_auth/user/create/'
|
id: '/_auth/user/create/'
|
||||||
path: '/user/create'
|
path: '/user/create'
|
||||||
|
|
@ -406,6 +419,13 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof AuthUserChangePasswordUserNameIndexRouteImport
|
preLoaderRoute: typeof AuthUserChangePasswordUserNameIndexRouteImport
|
||||||
parentRoute: typeof AuthRoute
|
parentRoute: typeof AuthRoute
|
||||||
}
|
}
|
||||||
|
'/_auth/rooms/$roomName/folder-status/': {
|
||||||
|
id: '/_auth/rooms/$roomName/folder-status/'
|
||||||
|
path: '/rooms/$roomName/folder-status'
|
||||||
|
fullPath: '/rooms/$roomName/folder-status'
|
||||||
|
preLoaderRoute: typeof AuthRoomsRoomNameFolderStatusIndexRouteImport
|
||||||
|
parentRoute: typeof AuthRoute
|
||||||
|
}
|
||||||
'/_auth/role/$id/edit/': {
|
'/_auth/role/$id/edit/': {
|
||||||
id: '/_auth/role/$id/edit/'
|
id: '/_auth/role/$id/edit/'
|
||||||
path: '/role/$id/edit'
|
path: '/role/$id/edit'
|
||||||
|
|
@ -425,13 +445,14 @@ interface AuthRouteChildren {
|
||||||
AuthDeviceIndexRoute: typeof AuthDeviceIndexRoute
|
AuthDeviceIndexRoute: typeof AuthDeviceIndexRoute
|
||||||
AuthRoleIndexRoute: typeof AuthRoleIndexRoute
|
AuthRoleIndexRoute: typeof AuthRoleIndexRoute
|
||||||
AuthRoomsIndexRoute: typeof AuthRoomsIndexRoute
|
AuthRoomsIndexRoute: typeof AuthRoomsIndexRoute
|
||||||
|
AuthUserIndexRoute: typeof AuthUserIndexRoute
|
||||||
AuthProfileUserNameIndexRoute: typeof AuthProfileUserNameIndexRoute
|
AuthProfileUserNameIndexRoute: typeof AuthProfileUserNameIndexRoute
|
||||||
AuthProfileChangePasswordIndexRoute: typeof AuthProfileChangePasswordIndexRoute
|
AuthProfileChangePasswordIndexRoute: typeof AuthProfileChangePasswordIndexRoute
|
||||||
AuthRoleCreateIndexRoute: typeof AuthRoleCreateIndexRoute
|
AuthRoleCreateIndexRoute: typeof AuthRoleCreateIndexRoute
|
||||||
AuthRoomsRoomNameIndexRoute: typeof AuthRoomsRoomNameIndexRoute
|
AuthRoomsRoomNameIndexRoute: typeof AuthRoomsRoomNameIndexRoute
|
||||||
AuthUserCreateIndexRoute: typeof AuthUserCreateIndexRoute
|
AuthUserCreateIndexRoute: typeof AuthUserCreateIndexRoute
|
||||||
AuthUserRoleIndexRoute: typeof AuthUserRoleIndexRoute
|
|
||||||
AuthRoleIdEditIndexRoute: typeof AuthRoleIdEditIndexRoute
|
AuthRoleIdEditIndexRoute: typeof AuthRoleIdEditIndexRoute
|
||||||
|
AuthRoomsRoomNameFolderStatusIndexRoute: typeof AuthRoomsRoomNameFolderStatusIndexRoute
|
||||||
AuthUserChangePasswordUserNameIndexRoute: typeof AuthUserChangePasswordUserNameIndexRoute
|
AuthUserChangePasswordUserNameIndexRoute: typeof AuthUserChangePasswordUserNameIndexRoute
|
||||||
AuthUserRoleRoleIdIndexRoute: typeof AuthUserRoleRoleIdIndexRoute
|
AuthUserRoleRoleIdIndexRoute: typeof AuthUserRoleRoleIdIndexRoute
|
||||||
}
|
}
|
||||||
|
|
@ -445,13 +466,15 @@ const AuthRouteChildren: AuthRouteChildren = {
|
||||||
AuthDeviceIndexRoute: AuthDeviceIndexRoute,
|
AuthDeviceIndexRoute: AuthDeviceIndexRoute,
|
||||||
AuthRoleIndexRoute: AuthRoleIndexRoute,
|
AuthRoleIndexRoute: AuthRoleIndexRoute,
|
||||||
AuthRoomsIndexRoute: AuthRoomsIndexRoute,
|
AuthRoomsIndexRoute: AuthRoomsIndexRoute,
|
||||||
|
AuthUserIndexRoute: AuthUserIndexRoute,
|
||||||
AuthProfileUserNameIndexRoute: AuthProfileUserNameIndexRoute,
|
AuthProfileUserNameIndexRoute: AuthProfileUserNameIndexRoute,
|
||||||
AuthProfileChangePasswordIndexRoute: AuthProfileChangePasswordIndexRoute,
|
AuthProfileChangePasswordIndexRoute: AuthProfileChangePasswordIndexRoute,
|
||||||
AuthRoleCreateIndexRoute: AuthRoleCreateIndexRoute,
|
AuthRoleCreateIndexRoute: AuthRoleCreateIndexRoute,
|
||||||
AuthRoomsRoomNameIndexRoute: AuthRoomsRoomNameIndexRoute,
|
AuthRoomsRoomNameIndexRoute: AuthRoomsRoomNameIndexRoute,
|
||||||
AuthUserCreateIndexRoute: AuthUserCreateIndexRoute,
|
AuthUserCreateIndexRoute: AuthUserCreateIndexRoute,
|
||||||
AuthUserRoleIndexRoute: AuthUserRoleIndexRoute,
|
|
||||||
AuthRoleIdEditIndexRoute: AuthRoleIdEditIndexRoute,
|
AuthRoleIdEditIndexRoute: AuthRoleIdEditIndexRoute,
|
||||||
|
AuthRoomsRoomNameFolderStatusIndexRoute:
|
||||||
|
AuthRoomsRoomNameFolderStatusIndexRoute,
|
||||||
AuthUserChangePasswordUserNameIndexRoute:
|
AuthUserChangePasswordUserNameIndexRoute:
|
||||||
AuthUserChangePasswordUserNameIndexRoute,
|
AuthUserChangePasswordUserNameIndexRoute,
|
||||||
AuthUserRoleRoleIdIndexRoute: AuthUserRoleRoleIdIndexRoute,
|
AuthUserRoleRoleIdIndexRoute: AuthUserRoleRoleIdIndexRoute,
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,6 @@ function AgentsPage() {
|
||||||
const columns: ColumnDef<Version>[] = [
|
const columns: ColumnDef<Version>[] = [
|
||||||
{ accessorKey: "version", header: "Phiên bản" },
|
{ accessorKey: "version", header: "Phiên bản" },
|
||||||
{ accessorKey: "fileName", header: "Tên file" },
|
{ accessorKey: "fileName", header: "Tên file" },
|
||||||
{ accessorKey: "folderPath", header: "Đường dẫn" },
|
|
||||||
{
|
{
|
||||||
accessorKey: "updatedAt",
|
accessorKey: "updatedAt",
|
||||||
header: "Thời gian cập nhật",
|
header: "Thời gian cập nhật",
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,6 @@ function AppsComponent() {
|
||||||
const columns: ColumnDef<Version>[] = [
|
const columns: ColumnDef<Version>[] = [
|
||||||
{ accessorKey: "version", header: "Phiên bản" },
|
{ accessorKey: "version", header: "Phiên bản" },
|
||||||
{ accessorKey: "fileName", header: "Tên file" },
|
{ accessorKey: "fileName", header: "Tên file" },
|
||||||
{ accessorKey: "folderPath", header: "Đường dẫn" },
|
|
||||||
{
|
{
|
||||||
accessorKey: "updatedAt",
|
accessorKey: "updatedAt",
|
||||||
header: () => <div className="whitespace-normal max-w-xs">Thời gian cập nhật</div>,
|
header: () => <div className="whitespace-normal max-w-xs">Thời gian cập nhật</div>,
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@ import {
|
||||||
useSendCommand,
|
useSendCommand,
|
||||||
} from "@/hooks/queries";
|
} from "@/hooks/queries";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
|
||||||
import { Check, X, Edit2, Trash2 } from "lucide-react";
|
import { Check, X, Edit2, Trash2 } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import type { ShellCommandData } from "@/components/forms/command-form";
|
import type { ShellCommandData } from "@/components/forms/command-form";
|
||||||
import type { CommandRegistry } from "@/types/command-registry";
|
import type { CommandRegistry } from "@/types/command-registry";
|
||||||
|
|
@ -22,8 +22,23 @@ export const Route = createFileRoute("/_auth/commands/")({
|
||||||
head: () => ({ meta: [{ title: "Gửi lệnh từ xa" }] }),
|
head: () => ({ meta: [{ title: "Gửi lệnh từ xa" }] }),
|
||||||
component: CommandPage,
|
component: CommandPage,
|
||||||
loader: async ({ context }) => {
|
loader: async ({ context }) => {
|
||||||
|
// Read active tab from URL search params (client-side) to reflect breadcrumb
|
||||||
|
let activeTab = "list";
|
||||||
|
try {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
activeTab = params.get("tab") || "list";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
activeTab = "list";
|
||||||
|
}
|
||||||
|
|
||||||
context.breadcrumbs = [
|
context.breadcrumbs = [
|
||||||
{ title: "Quản lý lệnh", path: "/_auth/commands/" },
|
{ title: "Quản lý lệnh", path: "/_auth/commands/" },
|
||||||
|
{
|
||||||
|
title: activeTab === "execute" ? "Lệnh thủ công" : "Danh sách",
|
||||||
|
path: `/ _auth/commands/?tab=${activeTab}`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -31,7 +46,6 @@ export const Route = createFileRoute("/_auth/commands/")({
|
||||||
function CommandPage() {
|
function CommandPage() {
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
const [selectedCommand, setSelectedCommand] = useState<CommandRegistry | null>(null);
|
const [selectedCommand, setSelectedCommand] = useState<CommandRegistry | null>(null);
|
||||||
const [detailPanelCommand, setDetailPanelCommand] = useState<CommandRegistry | null>(null);
|
|
||||||
const [table, setTable] = useState<any>();
|
const [table, setTable] = useState<any>();
|
||||||
|
|
||||||
// Fetch commands
|
// Fetch commands
|
||||||
|
|
@ -58,13 +72,16 @@ function CommandPage() {
|
||||||
const columns: ColumnDef<CommandRegistry>[] = [
|
const columns: ColumnDef<CommandRegistry>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "commandName",
|
accessorKey: "commandName",
|
||||||
header: "Tên lệnh",
|
header: () => <div className="min-w-[220px] whitespace-normal">Tên lệnh</div>,
|
||||||
size: 100,
|
size: 100,
|
||||||
cell: ({ getValue }) => (
|
cell: ({ getValue, row }) => {
|
||||||
<div className="max-w-[100px]">
|
const full = (getValue() as string) || row.original.commandName || "";
|
||||||
<span className="font-semibold truncate block">{getValue() as string}</span>
|
return (
|
||||||
|
<div className="min-w-[220px] whitespace-normal break-words">
|
||||||
|
<span className="font-semibold block leading-tight">{full}</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "commandType",
|
accessorKey: "commandType",
|
||||||
|
|
@ -80,18 +97,6 @@ function CommandPage() {
|
||||||
return <span>{typeMap[type] || "UNKNOWN"}</span>;
|
return <span>{typeMap[type] || "UNKNOWN"}</span>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
accessorKey: "description",
|
|
||||||
header: "Mô tả",
|
|
||||||
size: 120,
|
|
||||||
cell: ({ getValue }) => (
|
|
||||||
<div className="max-w-[120px]">
|
|
||||||
<span className="text-sm text-muted-foreground truncate block">
|
|
||||||
{(getValue() as string) || "-"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
accessorKey: "commandContent",
|
accessorKey: "commandContent",
|
||||||
header: "Nội dung lệnh",
|
header: "Nội dung lệnh",
|
||||||
|
|
@ -140,7 +145,7 @@ function CommandPage() {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "select",
|
id: "select",
|
||||||
header: () => <div className="text-center text-xs">Chọn để thực thi</div>,
|
header: () => <div className="text-center text-xs">Thực thi</div>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|
@ -283,7 +288,7 @@ function CommandPage() {
|
||||||
<>
|
<>
|
||||||
<CommandSubmitTemplate
|
<CommandSubmitTemplate
|
||||||
title="Gửi lệnh từ xa"
|
title="Gửi lệnh từ xa"
|
||||||
description="Quản lý và thực thi các lệnh trên thiết bị"
|
description="Quản lý và gửi yêu cầu thực thi các lệnh trên thiết bị"
|
||||||
data={commandList}
|
data={commandList}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|
@ -307,100 +312,11 @@ function CommandPage() {
|
||||||
onExecuteCustom={handleExecuteCustom}
|
onExecuteCustom={handleExecuteCustom}
|
||||||
isExecuting={sendCommandMutation.isPending}
|
isExecuting={sendCommandMutation.isPending}
|
||||||
rooms={roomData}
|
rooms={roomData}
|
||||||
onRowClick={(row) => setDetailPanelCommand(row)}
|
|
||||||
scrollable={true}
|
scrollable={true}
|
||||||
maxHeight="500px"
|
maxHeight="500px"
|
||||||
enablePagination
|
enablePagination={false}
|
||||||
defaultPageSize={10}
|
defaultPageSize={10}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Detail Dialog Popup */}
|
|
||||||
<Dialog open={!!detailPanelCommand} onOpenChange={(open) => !open && setDetailPanelCommand(null)}>
|
|
||||||
<DialogContent className="max-w-2xl max-h-[85vh]">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Chi tiết lệnh</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
{detailPanelCommand && (
|
|
||||||
<div className="space-y-6 max-h-[calc(85vh-120px)] overflow-y-auto pr-2">
|
|
||||||
{/* Command Name */}
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Tên lệnh</h3>
|
|
||||||
<p className="text-base font-medium break-words">{detailPanelCommand.commandName}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Command Type */}
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Loại lệnh</h3>
|
|
||||||
<p className="text-base">
|
|
||||||
{
|
|
||||||
{
|
|
||||||
1: "RESTART",
|
|
||||||
2: "SHUTDOWN",
|
|
||||||
3: "TASKKILL",
|
|
||||||
4: "BLOCK",
|
|
||||||
}[detailPanelCommand.commandType] || "UNKNOWN"
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Mô tả</h3>
|
|
||||||
<p className="text-sm text-foreground whitespace-pre-wrap break-words">
|
|
||||||
{detailPanelCommand.description || "-"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Command Content */}
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Nội dung lệnh</h3>
|
|
||||||
<div className="bg-muted/50 p-4 rounded-md border">
|
|
||||||
<code className="text-sm whitespace-pre-wrap break-all block font-mono">
|
|
||||||
{detailPanelCommand.commandContent}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* QoS */}
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-semibold text-muted-foreground mb-2">QoS</h3>
|
|
||||||
<p className="text-base">
|
|
||||||
<span
|
|
||||||
className={
|
|
||||||
{
|
|
||||||
0: "text-blue-600",
|
|
||||||
1: "text-amber-600",
|
|
||||||
2: "text-red-600",
|
|
||||||
}[(detailPanelCommand.qoS ?? 0) as 0 | 1 | 2]
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{detailPanelCommand.qoS ?? 0}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Retention */}
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Lưu trữ</h3>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{detailPanelCommand.isRetained ? (
|
|
||||||
<>
|
|
||||||
<Check className="h-4 w-4 text-green-600" />
|
|
||||||
<span className="text-sm text-green-600">Có</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<X className="h-4 w-4 text-gray-400" />
|
|
||||||
<span className="text-sm text-gray-400">Không</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
99
src/routes/_auth/rooms/$roomName/folder-status/index.tsx
Normal file
99
src/routes/_auth/rooms/$roomName/folder-status/index.tsx
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import {
|
||||||
|
createFileRoute,
|
||||||
|
useParams,
|
||||||
|
useNavigate,
|
||||||
|
} from "@tanstack/react-router";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useGetClientFolderStatus } from "@/hooks/queries";
|
||||||
|
import type { ClientFolderStatus } from "@/types/folder";
|
||||||
|
import FolderStatusTemplate from "@/template/folder-status-template";
|
||||||
|
import {
|
||||||
|
createColumnHelper,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_auth/rooms/$roomName/folder-status/")({
|
||||||
|
head: ({ params }) => ({
|
||||||
|
meta: [{ title: `Trạng thái thư mục Setup phòng ${params.roomName}` }],
|
||||||
|
}),
|
||||||
|
loader: async ({ context, params }) => {
|
||||||
|
context.breadcrumbs = [
|
||||||
|
{ title: "Danh sách phòng", path: "/rooms/" },
|
||||||
|
{ title: `Phòng ${params.roomName}`, path: `/rooms/${params.roomName}/` },
|
||||||
|
{ title: `Trạng thái thư mục Setup phòng ${params.roomName}`, path: `/rooms/${params.roomName}/folder-status/` },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
component: RouteComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const { roomName } = useParams({
|
||||||
|
from: "/_auth/rooms/$roomName/folder-status/",
|
||||||
|
});
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { data: folderStatusList = [], isLoading } = useGetClientFolderStatus(
|
||||||
|
roomName as string,
|
||||||
|
);
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<ClientFolderStatus>();
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.accessor("deviceId", {
|
||||||
|
header: "Máy tính",
|
||||||
|
cell: (info) => info.getValue() ?? "-",
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "missing",
|
||||||
|
header: "Số lượng file thiếu",
|
||||||
|
cell: (info) =>
|
||||||
|
(info.row.original.missingFiles?.length ?? 0).toString(),
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "extra",
|
||||||
|
header: "Số lượng file thừa",
|
||||||
|
cell: (info) => (info.row.original.extraFiles?.length ?? 0).toString(),
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "current",
|
||||||
|
header: "Số lượng file hiện tại",
|
||||||
|
cell: (info) =>
|
||||||
|
(info.row.original.currentFiles?.length ?? 0).toString(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("updatedAt", {
|
||||||
|
header: "Updated",
|
||||||
|
cell: (info) => {
|
||||||
|
const v = info.getValue();
|
||||||
|
try {
|
||||||
|
const d = new Date(v as string);
|
||||||
|
return isNaN(d.getTime())
|
||||||
|
? (v as string)
|
||||||
|
: d.toLocaleString("vi-VN");
|
||||||
|
} catch {
|
||||||
|
return v as string;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: folderStatusList ?? [],
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FolderStatusTemplate
|
||||||
|
roomName={roomName as string}
|
||||||
|
data={folderStatusList}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onBack={() =>
|
||||||
|
navigate({ to: "/rooms/$roomName/", params: { roomName } } as any)
|
||||||
|
}
|
||||||
|
table={table}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,62 +1,41 @@
|
||||||
import { createFileRoute, useParams } from "@tanstack/react-router";
|
import { createFileRoute, useParams, useNavigate } from "@tanstack/react-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { LayoutGrid, TableIcon, Monitor, FolderCheck, Loader2 } from "lucide-react";
|
import { LayoutGrid, TableIcon, Monitor, FolderCheck } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useGetDeviceFromRoom } from "@/hooks/queries";
|
import { useGetDeviceFromRoom } from "@/hooks/queries";
|
||||||
import { useDeviceEvents } from "@/hooks/useDeviceEvents";
|
import { useDeviceEvents } from "@/hooks/useDeviceEvents";
|
||||||
import { useClientFolderStatus } from "@/hooks/useClientFolderStatus";
|
|
||||||
import { DeviceGrid } from "@/components/grids/device-grid";
|
import { DeviceGrid } from "@/components/grids/device-grid";
|
||||||
import { DeviceTable } from "@/components/tables/device-table";
|
import { DeviceTable } from "@/components/tables/device-table";
|
||||||
import { useMachineNumber } from "@/hooks/useMachineNumber";
|
import { useMachineNumber } from "@/hooks/useMachineNumber";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { CommandActionButtons } from "@/components/buttons/command-action-buttons";
|
import { CommandActionButtons } from "@/components/buttons/command-action-buttons";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_auth/rooms/$roomName/")({
|
export const Route = createFileRoute("/_auth/rooms/$roomName/")({
|
||||||
head: ({ params }) => ({
|
head: ({ params }) => ({
|
||||||
meta: [{ title: `Danh sách thiết bị phòng ${params.roomName}` }],
|
meta: [{ title: `Danh sách thiết bị phòng ${params.roomName}` }],
|
||||||
}),
|
}),
|
||||||
|
loader: async ({ context, params }) => {
|
||||||
|
context.breadcrumbs = [
|
||||||
|
{ title: "Danh sách phòng", path: "/rooms/" },
|
||||||
|
{ title: `Phòng ${params.roomName}`, path: `/rooms/${params.roomName}/` },
|
||||||
|
];
|
||||||
|
},
|
||||||
component: RoomDetailPage,
|
component: RoomDetailPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
function RoomDetailPage() {
|
function RoomDetailPage() {
|
||||||
const { roomName } = useParams({ from: "/_auth/rooms/$roomName/" });
|
const { roomName } = useParams({ from: "/_auth/rooms/$roomName/" });
|
||||||
const [viewMode, setViewMode] = useState<"grid" | "table">("grid");
|
const [viewMode, setViewMode] = useState<"grid" | "table">("grid");
|
||||||
const [isCheckingFolder, setIsCheckingFolder] = useState(false);
|
|
||||||
|
|
||||||
// SSE real-time updates
|
// SSE real-time updates
|
||||||
useDeviceEvents(roomName);
|
useDeviceEvents(roomName);
|
||||||
|
|
||||||
// Folder status from SSE
|
// Folder status from SS
|
||||||
const folderStatuses = useClientFolderStatus(roomName);
|
|
||||||
|
|
||||||
const { data: devices = [] } = useGetDeviceFromRoom(roomName);
|
const { data: devices = [] } = useGetDeviceFromRoom(roomName);
|
||||||
|
|
||||||
const parseMachineNumber = useMachineNumber();
|
const parseMachineNumber = useMachineNumber();
|
||||||
|
|
||||||
const handleCheckFolderStatus = async () => {
|
const navigate = useNavigate();
|
||||||
try {
|
|
||||||
setIsCheckingFolder(true);
|
|
||||||
// Trigger folder status check via the service
|
|
||||||
const response = await fetch(
|
|
||||||
`/api/device-comm/request-get-client-folder-status?roomName=${encodeURIComponent(roomName)}`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to request folder status");
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success("Đang kiểm tra thư mục Setup...");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Check folder error:", error);
|
|
||||||
toast.error("Lỗi khi kiểm tra thư mục!");
|
|
||||||
} finally {
|
|
||||||
setIsCheckingFolder(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const sortedDevices = [...devices].sort((a, b) => {
|
const sortedDevices = [...devices].sort((a, b) => {
|
||||||
return parseMachineNumber(a.id) - parseMachineNumber(b.id);
|
return parseMachineNumber(a.id) - parseMachineNumber(b.id);
|
||||||
|
|
@ -101,7 +80,7 @@ function RoomDetailPage() {
|
||||||
Thực thi lệnh
|
Thực thi lệnh
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3 flex-wrap justify-end">
|
<div className="flex items-center gap-3 justify-end">
|
||||||
{/* Command Action Buttons */}
|
{/* Command Action Buttons */}
|
||||||
{devices.length > 0 && (
|
{devices.length > 0 && (
|
||||||
<>
|
<>
|
||||||
|
|
@ -110,18 +89,18 @@ function RoomDetailPage() {
|
||||||
<div className="h-8 w-px bg-border" />
|
<div className="h-8 w-px bg-border" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleCheckFolderStatus}
|
onClick={() =>
|
||||||
disabled={isCheckingFolder}
|
navigate({
|
||||||
|
to: "/rooms/$roomName/folder-status/",
|
||||||
|
params: { roomName },
|
||||||
|
} as any)
|
||||||
|
}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex items-center gap-2 shrink-0"
|
className="flex items-center gap-2 shrink-0"
|
||||||
>
|
>
|
||||||
{isCheckingFolder ? (
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<FolderCheck className="h-4 w-4" />
|
<FolderCheck className="h-4 w-4" />
|
||||||
)}
|
Kiểm tra thư mục Setup
|
||||||
{isCheckingFolder ? "Đang kiểm tra..." : "Kiểm tra thư mục Setup"}
|
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
@ -141,18 +120,15 @@ function RoomDetailPage() {
|
||||||
) : viewMode === "grid" ? (
|
) : viewMode === "grid" ? (
|
||||||
<DeviceGrid
|
<DeviceGrid
|
||||||
devices={sortedDevices}
|
devices={sortedDevices}
|
||||||
folderStatuses={folderStatuses}
|
|
||||||
isCheckingFolder={isCheckingFolder}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<DeviceTable
|
<DeviceTable
|
||||||
devices={sortedDevices}
|
devices={sortedDevices}
|
||||||
folderStatuses={folderStatuses}
|
|
||||||
isCheckingFolder={isCheckingFolder}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,9 @@ export const Route = createFileRoute("/_auth/rooms/")({
|
||||||
head: () => ({
|
head: () => ({
|
||||||
meta: [{ title: "Danh sách phòng" }],
|
meta: [{ title: "Danh sách phòng" }],
|
||||||
}),
|
}),
|
||||||
|
loader: async ({ context }) => {
|
||||||
|
context.breadcrumbs = [{ title: "Danh sách phòng", path: "#" }];
|
||||||
|
},
|
||||||
component: RoomComponent,
|
component: RoomComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ function CreateUserComponent() {
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => navigate({ to: "/dashboard" })}
|
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
|
||||||
|
|
|
||||||
165
src/routes/_auth/user/index.tsx
Normal file
165
src/routes/_auth/user/index.tsx
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { UserListTemplate } from "@/template/user-list-template";
|
||||||
|
import { useGetUsersInfo } from "@/hooks/queries";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipTrigger,
|
||||||
|
TooltipContent,
|
||||||
|
} 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 { toast } from "sonner";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_auth/user/")({
|
||||||
|
component: RouteComponent,
|
||||||
|
loader: async ({ context }) => {
|
||||||
|
context.breadcrumbs = [
|
||||||
|
{ title: "Quản lý người dùng", path: "#" },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { data: users = [], isLoading } = useGetUsersInfo();
|
||||||
|
|
||||||
|
const [table, setTable] = useState<any>();
|
||||||
|
|
||||||
|
const columns = useMemo<ColumnDef<any>[]>(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
accessorKey: "userName",
|
||||||
|
header: () => <div className="text-center whitespace-normal max-w-xs">Tên đăng nhập</div>,
|
||||||
|
cell: ({ getValue }) => (
|
||||||
|
<div className="flex justify-center"><span className="font-medium">{getValue() as string}</span></div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "name",
|
||||||
|
header: () => <div className="text-center whitespace-normal max-w-xs">Họ và tên</div>,
|
||||||
|
cell: ({ getValue }) => (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span className="truncate block max-w-[240px]">
|
||||||
|
{getValue() as string}
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom">
|
||||||
|
{getValue() as string}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "role",
|
||||||
|
header: () => <div className="text-center whitespace-normal max-w-xs">Vai trò</div>,
|
||||||
|
cell: ({ getValue }) => <div className="flex justify-center">{getValue() as string}</div>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "accessRooms",
|
||||||
|
header: () => <div className="text-center whitespace-normal max-w-xs">Phòng</div>,
|
||||||
|
cell: ({ getValue }) => (
|
||||||
|
<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: () => (
|
||||||
|
<div className="text-center whitespace-normal max-w-xs">Hành động</div>
|
||||||
|
),
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
enableHiding: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[navigate],
|
||||||
|
);
|
||||||
|
|
||||||
|
const tableData = users.map((u) => ({ ...u, id: u.userName }));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserListTemplate
|
||||||
|
title="Người dùng"
|
||||||
|
description="Danh sách tài khoản hệ thống"
|
||||||
|
headerAction={
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={() => navigate({ to: "/user/create" } as any)}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
Thêm người dùng
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="p-4">
|
||||||
|
<VersionTable
|
||||||
|
data={tableData}
|
||||||
|
isLoading={isLoading}
|
||||||
|
columns={columns as ColumnDef<any, any>[]}
|
||||||
|
scrollable={true}
|
||||||
|
maxHeight="400px"
|
||||||
|
enablePagination={false}
|
||||||
|
onTableInit={(t) => setTable(t)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</UserListTemplate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -13,11 +13,24 @@ import { Shield, ArrowLeft, Check, X } from "lucide-react";
|
||||||
import type { PermissionOnRole } from "@/types/permission";
|
import type { PermissionOnRole } from "@/types/permission";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_auth/user/role/$roleId/")({
|
export const Route = createFileRoute("/_auth/user/role/$roleId/")({
|
||||||
|
head: () => ({
|
||||||
|
meta: [{ title: "Quyền của người dùng | AccessControl" }]
|
||||||
|
}),
|
||||||
component: ViewRolePermissionsComponent,
|
component: ViewRolePermissionsComponent,
|
||||||
loader: async ({ context, params }) => {
|
loader: async ({ context, params }) => {
|
||||||
context.breadcrumbs = [
|
context.breadcrumbs = [
|
||||||
{ title: "Quản lý người dùng", path: "#" },
|
{
|
||||||
{ title: `Quyền của Role #${params.roleId}`, path: `/user/role/${params.roleId}` },
|
title: "Quản lý tài khoản",
|
||||||
|
path: "#"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Danh sách người dùng",
|
||||||
|
path: "/user"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Quyền của người dùng",
|
||||||
|
path: `/user/role/${params.roleId}`
|
||||||
|
}
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -60,7 +73,7 @@ function ViewRolePermissionsComponent() {
|
||||||
return (
|
return (
|
||||||
<div className="w-full px-6 space-y-4">
|
<div className="w-full px-6 space-y-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Button variant="ghost" size="sm" onClick={() => navigate({ to: "/dashboard" })}>
|
<Button variant="ghost" size="sm" 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>
|
||||||
|
|
@ -88,11 +101,11 @@ function ViewRolePermissionsComponent() {
|
||||||
<div key={parent?.permisionId} className="border rounded-lg p-4">
|
<div key={parent?.permisionId} className="border rounded-lg p-4">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-semibold text-lg">{parent?.permissionName || "Unknown"}</span>
|
<span className="font-semibold text-lg">{parent?.permissionName || "Allow all"}</span>
|
||||||
<Badge variant="secondary" className="text-xs">{parent?.permissionCode}</Badge>
|
<Badge variant="secondary" className="text-xs">{parent?.permissionCode}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{parent?.isChecked === 1 ? (
|
{(parent?.isChecked === 1 || parent === null) ? (
|
||||||
<Badge variant="default" className="bg-green-600">
|
<Badge variant="default" className="bg-green-600">
|
||||||
<Check className="h-3 w-3 mr-1" />Đã bật
|
<Check className="h-3 w-3 mr-1" />Đã bật
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
|
||||||
import { useGetRoleList } from "@/hooks/queries";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import { Shield, Eye, ArrowLeft, Loader2 } from "lucide-react";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/_auth/user/role/")({
|
|
||||||
component: RoleListComponent,
|
|
||||||
loader: async ({ context }) => {
|
|
||||||
context.breadcrumbs = [
|
|
||||||
{ title: "Quản lý người dùng", path: "#" },
|
|
||||||
{ title: "Danh sách vai trò", path: "/user/role" },
|
|
||||||
];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function RoleListComponent() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { data: roles, isLoading, isError } = useGetRoleList();
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="w-full px-6 flex items-center justify-center py-12">
|
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isError) {
|
|
||||||
return (
|
|
||||||
<div className="w-full px-6 space-y-4">
|
|
||||||
<Card className="max-w-2xl mx-auto">
|
|
||||||
<CardContent className="pt-6">
|
|
||||||
<div className="text-center text-destructive">
|
|
||||||
Không thể tải danh sách vai trò
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-center mt-4">
|
|
||||||
<Button variant="outline" onClick={() => navigate({ to: "/dashboard" })}>
|
|
||||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
|
||||||
Quay lại
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full px-6 space-y-4">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Button variant="ghost" size="sm" onClick={() => navigate({ to: "/dashboard" })}>
|
|
||||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
|
||||||
Quay lại
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card className="max-w-4xl mx-auto">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<Shield className="h-5 w-5" />
|
|
||||||
Danh sách vai trò
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Quản lý các vai trò và quyền hạn trong hệ thống
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{roles && roles.length > 0 ? (
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>ID</TableHead>
|
|
||||||
<TableHead>Tên vai trò</TableHead>
|
|
||||||
<TableHead>Độ ưu tiên</TableHead>
|
|
||||||
<TableHead>Ngày tạo</TableHead>
|
|
||||||
<TableHead className="text-right">Thao tác</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{roles.map((role) => (
|
|
||||||
<TableRow key={role.id}>
|
|
||||||
<TableCell>
|
|
||||||
<Badge variant="outline">{role.id}</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="font-medium">{role.roleName}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge variant="secondary">{role.priority}</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-muted-foreground">
|
|
||||||
{role.createdAt ? new Date(role.createdAt).toLocaleDateString("vi-VN") : "—"}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
navigate({ to: "/user/role/$roleId", params: { roleId: String(role.id) } } as any)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Eye className="h-4 w-4 mr-1" />
|
|
||||||
Xem quyền
|
|
||||||
</Button>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
|
||||||
Không có vai trò nào trong hệ thống
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -111,7 +111,7 @@ export async function addRequiredFile(data: any): Promise<{ message: string }> {
|
||||||
* @param fileId - ID file
|
* @param fileId - ID file
|
||||||
*/
|
*/
|
||||||
export async function deleteRequiredFile(fileId: number): Promise<{ message: string }> {
|
export async function deleteRequiredFile(fileId: number): Promise<{ message: string }> {
|
||||||
const response = await axios.delete(
|
const response = await axios.post(
|
||||||
API_ENDPOINTS.APP_VERSION.DELETE_REQUIRED_FILE(fileId)
|
API_ENDPOINTS.APP_VERSION.DELETE_REQUIRED_FILE(fileId)
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|
|
||||||
|
|
@ -51,3 +51,22 @@ export async function deleteCommand(commandId: number): Promise<any> {
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lấy danh sách lệnh nhạy cảm
|
||||||
|
* @return - Danh sách lệnh nhạy cảm
|
||||||
|
* */
|
||||||
|
export async function getSensitiveCommands(): Promise<any[]> {
|
||||||
|
const response = await axios.get<any[]>(API_ENDPOINTS.COMMAND.GET_SENSITIVE_COMMANDS);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gửi yêu cầu thực thi lệnh nhạy cảm
|
||||||
|
* @param data - Dữ liệu lệnh nhạy cảm
|
||||||
|
* @return - Kết quả thực thi
|
||||||
|
* */
|
||||||
|
export async function requestSendSensitiveCommand(data: any): Promise<any> {
|
||||||
|
const response = await axios.post(API_ENDPOINTS.COMMAND.REQUEST_SEND_SENSITIVE_COMMAND, data);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
|
||||||
34
src/services/user.service.ts
Normal file
34
src/services/user.service.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import axios from "@/config/axios";
|
||||||
|
import { API_ENDPOINTS } from "@/config/api";
|
||||||
|
import type { UserProfile } from "@/types/user-profile";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lấy danh sách thông tin người dùng và chuyển sang camelCase keys
|
||||||
|
*/
|
||||||
|
export async function getUsersInfo(): Promise<UserProfile[]> {
|
||||||
|
try {
|
||||||
|
const response = await axios.get<any[]>(API_ENDPOINTS.AUTH.GET_USERS_LIST);
|
||||||
|
const list = Array.isArray(response.data) ? response.data : [];
|
||||||
|
|
||||||
|
return list.map((u: any) => ({
|
||||||
|
userName: u.userName ?? u.UserName ?? "",
|
||||||
|
name: u.name ?? u.Name ?? "",
|
||||||
|
role: u.role ?? u.Role ?? "",
|
||||||
|
roleId: u.roleId !== undefined ? Number(u.roleId) : u.RoleId !== undefined ? Number(u.RoleId) : 0,
|
||||||
|
accessRooms: Array.isArray(u.accessRooms)
|
||||||
|
? u.accessRooms.map((v: any) => Number(v))
|
||||||
|
: Array.isArray(u.AccessRooms)
|
||||||
|
? u.AccessRooms.map((v: any) => Number(v))
|
||||||
|
: [],
|
||||||
|
createdAt: u.createdAt ?? u.CreatedAt ?? null,
|
||||||
|
createdBy: u.createdBy ?? u.CreatedBy ?? null,
|
||||||
|
updatedAt: u.updatedAt ?? u.UpdatedAt ?? null,
|
||||||
|
updatedBy: u.updatedBy ?? u.UpdatedBy ?? null,
|
||||||
|
} as UserProfile));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("getUsersInfo error:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { getUsersInfo };
|
||||||
|
|
@ -96,12 +96,28 @@ export function CommandSubmitTemplate<T extends { id: number }>({
|
||||||
pageSizeOptions = [5, 10, 15, 20],
|
pageSizeOptions = [5, 10, 15, 20],
|
||||||
}: CommandSubmitTemplateProps<T>) {
|
}: CommandSubmitTemplateProps<T>) {
|
||||||
const [activeTab, setActiveTab] = useState<"list" | "execute">("list");
|
const [activeTab, setActiveTab] = useState<"list" | "execute">("list");
|
||||||
|
|
||||||
|
// Sync tab to URL search param so route breadcrumbs can reflect active tab
|
||||||
|
const setTab = (t: "list" | "execute") => {
|
||||||
|
setActiveTab(t);
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
try {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set("tab", t);
|
||||||
|
window.history.replaceState({}, "", url.toString());
|
||||||
|
} catch (e) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
const [customCommand, setCustomCommand] = useState("");
|
const [customCommand, setCustomCommand] = useState("");
|
||||||
const [customQoS, setCustomQoS] = useState<0 | 1 | 2>(0);
|
const [customQoS, setCustomQoS] = useState<0 | 1 | 2>(0);
|
||||||
const [customRetained, setCustomRetained] = useState(false);
|
const [customRetained, setCustomRetained] = useState(false);
|
||||||
const [table, setTable] = useState<any>();
|
const [table, setTable] = useState<any>();
|
||||||
const [dialogOpen2, setDialogOpen2] = useState(false);
|
const [dialogOpen2, setDialogOpen2] = useState(false);
|
||||||
const [dialogType, setDialogType] = useState<"room" | "device" | "room-custom" | "device-custom" | null>(null);
|
const [dialogType, setDialogType] = useState<
|
||||||
|
"room" | "device" | "room-custom" | "device-custom" | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
const handleTableInit = (t: any) => {
|
const handleTableInit = (t: any) => {
|
||||||
setTable(t);
|
setTable(t);
|
||||||
|
|
@ -141,7 +157,7 @@ export function CommandSubmitTemplate<T extends { id: number }>({
|
||||||
if (!onExecuteSelected) return;
|
if (!onExecuteSelected) return;
|
||||||
try {
|
try {
|
||||||
const roomNames = rooms.map((room) =>
|
const roomNames = rooms.map((room) =>
|
||||||
typeof room === "string" ? room : room.name
|
typeof room === "string" ? room : room.name,
|
||||||
);
|
);
|
||||||
const allTargets = [...roomNames, ...devices];
|
const allTargets = [...roomNames, ...devices];
|
||||||
onExecuteSelected(allTargets);
|
onExecuteSelected(allTargets);
|
||||||
|
|
@ -176,7 +192,7 @@ export function CommandSubmitTemplate<T extends { id: number }>({
|
||||||
if (!onExecuteCustom) return;
|
if (!onExecuteCustom) return;
|
||||||
try {
|
try {
|
||||||
const roomNames = rooms.map((room) =>
|
const roomNames = rooms.map((room) =>
|
||||||
typeof room === "string" ? room : room.name
|
typeof room === "string" ? room : room.name,
|
||||||
);
|
);
|
||||||
const allTargets = [...roomNames, ...devices];
|
const allTargets = [...roomNames, ...devices];
|
||||||
handleExecuteCustom(allTargets);
|
handleExecuteCustom(allTargets);
|
||||||
|
|
@ -227,7 +243,7 @@ export function CommandSubmitTemplate<T extends { id: number }>({
|
||||||
{/* Tabs Navigation */}
|
{/* Tabs Navigation */}
|
||||||
<div className="flex gap-4 border-b">
|
<div className="flex gap-4 border-b">
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab("list")}
|
onClick={() => setTab("list")}
|
||||||
className={`pb-3 px-2 font-medium text-sm flex items-center gap-2 transition-colors ${
|
className={`pb-3 px-2 font-medium text-sm flex items-center gap-2 transition-colors ${
|
||||||
activeTab === "list"
|
activeTab === "list"
|
||||||
? "text-primary border-b-2 border-primary"
|
? "text-primary border-b-2 border-primary"
|
||||||
|
|
@ -238,7 +254,7 @@ export function CommandSubmitTemplate<T extends { id: number }>({
|
||||||
Danh sách lệnh có sẵn
|
Danh sách lệnh có sẵn
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab("execute")}
|
onClick={() => setTab("execute")}
|
||||||
className={`pb-3 px-2 font-medium text-sm flex items-center gap-2 transition-colors ${
|
className={`pb-3 px-2 font-medium text-sm flex items-center gap-2 transition-colors ${
|
||||||
activeTab === "execute"
|
activeTab === "execute"
|
||||||
? "text-primary border-b-2 border-primary"
|
? "text-primary border-b-2 border-primary"
|
||||||
|
|
@ -436,7 +452,12 @@ export function CommandSubmitTemplate<T extends { id: number }>({
|
||||||
{/* Dialog for add/edit */}
|
{/* Dialog for add/edit */}
|
||||||
{formContent && (
|
{formContent && (
|
||||||
<Dialog open={dialogOpen} onOpenChange={onDialogOpen}>
|
<Dialog open={dialogOpen} onOpenChange={onDialogOpen}>
|
||||||
<DialogContent className={dialogContentClassName || "max-w-2xl max-h-[90vh] overflow-y-auto"}>
|
<DialogContent
|
||||||
|
className={
|
||||||
|
dialogContentClassName ||
|
||||||
|
"w-full max-w-[90vw] sm:max-w-[70vw] md:max-w-[50vw] max-h-[90vh] overflow-y-auto"
|
||||||
|
}
|
||||||
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{dialogTitle}</DialogTitle>
|
<DialogTitle>{dialogTitle}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
|
||||||
124
src/template/folder-status-template.tsx
Normal file
124
src/template/folder-status-template.tsx
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
import { flexRender } from "@tanstack/react-table";
|
||||||
|
import type { ClientFolderStatus, CurrentFile } from "@/types/folder";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
import { useState, Fragment } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
roomName: string;
|
||||||
|
data: ClientFolderStatus[];
|
||||||
|
isLoading?: boolean;
|
||||||
|
onBack: () => void;
|
||||||
|
table: any; // react-table instance (optional)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function FolderStatusTemplate({ roomName, data, isLoading, onBack, table }: Props) {
|
||||||
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||||
|
// If a table instance is provided (pre-built), use it; otherwise render simple table
|
||||||
|
if (table) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Button variant="ghost" size="sm" onClick={onBack}>
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
Trở về
|
||||||
|
</Button>
|
||||||
|
<h2 className="text-lg font-semibold">Kết quả kiểm tra thư mục Setup — {roomName}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded border">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead className="bg-muted/30">
|
||||||
|
{table.getHeaderGroups().map((hg: any) => (
|
||||||
|
<tr key={hg.id}>
|
||||||
|
{hg.headers.map((h: any) => (
|
||||||
|
<th key={h.id} className="text-left p-2">
|
||||||
|
{flexRender(h.column.columnDef.header, h.getContext())}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{table.getRowModel().rows.map((row: any) => (
|
||||||
|
<Fragment key={row.id}>
|
||||||
|
<tr
|
||||||
|
onClick={() => setExpandedId((prev) => (prev === row.id ? null : row.id))}
|
||||||
|
className="hover:bg-muted/20 cursor-pointer"
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell: any) => (
|
||||||
|
<td key={cell.id} className="p-2 align-top">
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{/* Detail row - shown when expanded */}
|
||||||
|
{expandedId === row.id && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={row.getVisibleCells().length} className="p-4 bg-muted/10">
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-muted-foreground mb-1">Các file đang có trong thư mục ({row.original.currentFiles?.length ?? 0})</div>
|
||||||
|
{(row.original.currentFiles ?? []).map((f: CurrentFile) => (
|
||||||
|
<div key={f.fileName} className="text-xs font-mono">{f.fileName}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback simple list view if table not provided
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Button variant="ghost" size="sm" onClick={onBack}>
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
Trở về
|
||||||
|
</Button>
|
||||||
|
<h2 className="text-lg font-semibold">Kết quả kiểm tra thư mục Setup — {roomName}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded border p-4">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="text-center text-muted-foreground">Đang tải...</div>
|
||||||
|
) : data.length === 0 ? (
|
||||||
|
<div className="text-center text-muted-foreground">Không có dữ liệu</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{data.map((item) => (
|
||||||
|
<div key={item.deviceId} className="p-3 rounded border">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="font-semibold">{item.deviceId}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">{new Date(item.updatedAt).toLocaleString("vi-VN")}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 grid grid-cols-3 gap-3">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-muted-foreground mb-1">Các file đang có trong thư mục ({item.currentFiles?.length ?? 0})</div>
|
||||||
|
{(item.currentFiles ?? []).map((f: CurrentFile) => (
|
||||||
|
<div key={f.fileName} className="text-xs font-mono">{f.fileName}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
src/template/user-list-template.tsx
Normal file
38
src/template/user-list-template.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter} from "@/components/ui/card";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
headerAction?: React.ReactNode;
|
||||||
|
footer?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function UserListTemplate({ title, description, children, headerAction, footer }: Props) {
|
||||||
|
return (
|
||||||
|
<div className="w-full px-6 space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">{title}</h1>
|
||||||
|
{description && <div className="text-muted-foreground mt-2">{description}</div>}
|
||||||
|
</div>
|
||||||
|
<div>{headerAction}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader>
|
||||||
|
<div>
|
||||||
|
<CardTitle className="flex items-center gap-2">Danh sách người dùng</CardTitle>
|
||||||
|
{description && <CardDescription>{description}</CardDescription>}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>{children}</CardContent>
|
||||||
|
{footer && <CardFooter>{footer}</CardFooter>}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserListTemplate;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { AppWindow, Building, CircleX, Home, ShieldCheck, Terminal, UserPlus} from "lucide-react";
|
import { AppWindow, Building, CircleX, Folder, Home, ShieldCheck, Terminal, UserPlus} from "lucide-react";
|
||||||
import { PermissionEnum } from "./permission";
|
import { PermissionEnum } from "./permission";
|
||||||
|
|
||||||
enum AppSidebarSectionCode {
|
enum AppSidebarSectionCode {
|
||||||
|
|
@ -18,10 +18,10 @@ export const appSidebarSection = {
|
||||||
versions: ["1.0.1", "1.1.0-alpha", "2.0.0-beta1"],
|
versions: ["1.0.1", "1.1.0-alpha", "2.0.0-beta1"],
|
||||||
navMain: [
|
navMain: [
|
||||||
{
|
{
|
||||||
title: "Thống kê tổng quan",
|
title: "Tổng quan",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "Dashboard",
|
title: "Thống kê",
|
||||||
url: "/dashboard",
|
url: "/dashboard",
|
||||||
code: AppSidebarSectionCode.DASHBOARD,
|
code: AppSidebarSectionCode.DASHBOARD,
|
||||||
icon: Home,
|
icon: Home,
|
||||||
|
|
@ -30,7 +30,7 @@ export const appSidebarSection = {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Quan lý phòng máy",
|
title: "Quản lý phòng máy",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "Danh sách phòng máy",
|
title: "Danh sách phòng máy",
|
||||||
|
|
@ -42,25 +42,25 @@ export const appSidebarSection = {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Agent và phần mềm",
|
title: "Quản lý agent/thư mục Setup",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "Danh sách Agent",
|
title: "Agent",
|
||||||
url: "/agent",
|
url: "/agent",
|
||||||
code: AppSidebarSectionCode.AGENT_MANAGEMENT,
|
code: AppSidebarSectionCode.AGENT_MANAGEMENT,
|
||||||
icon: AppWindow,
|
icon: AppWindow,
|
||||||
permissions: [PermissionEnum.VIEW_AGENT],
|
permissions: [PermissionEnum.VIEW_AGENT],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Quản lý phần mềm",
|
title: "Thư mục Setup",
|
||||||
url: "/apps",
|
url: "/apps",
|
||||||
icon: AppWindow,
|
icon: Folder,
|
||||||
permissions: [PermissionEnum.VIEW_APPS],
|
permissions: [PermissionEnum.VIEW_APPS],
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Lệnh và các ứng dụng bị chặn",
|
title: "Quản lý lệnh/blacklist",
|
||||||
items:
|
items:
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|
@ -70,7 +70,7 @@ export const appSidebarSection = {
|
||||||
permissions: [PermissionEnum.VIEW_COMMAND],
|
permissions: [PermissionEnum.VIEW_COMMAND],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Danh sách ứng dụng/web bị chặn",
|
title: "Chặn ứng dụng/website",
|
||||||
url: "/blacklists",
|
url: "/blacklists",
|
||||||
icon: CircleX,
|
icon: CircleX,
|
||||||
permissions: [PermissionEnum.ALLOW_ALL],
|
permissions: [PermissionEnum.ALLOW_ALL],
|
||||||
|
|
@ -78,7 +78,7 @@ export const appSidebarSection = {
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Phân quyền và người dùng",
|
title: "Quản lý tài khoản/phân quyền",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "Danh sách roles",
|
title: "Danh sách roles",
|
||||||
|
|
@ -87,10 +87,10 @@ export const appSidebarSection = {
|
||||||
permissions: [PermissionEnum.VIEW_ROLES],
|
permissions: [PermissionEnum.VIEW_ROLES],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Tạo người dùng",
|
title: "Danh sách người dùng",
|
||||||
url: "/user/create",
|
url: "/user",
|
||||||
icon: UserPlus,
|
icon: UserPlus,
|
||||||
permissions: [PermissionEnum.CRE_USER],
|
permissions: [PermissionEnum.VIEW_USER],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,5 @@ export enum CommandType {
|
||||||
SHUTDOWN = 2,
|
SHUTDOWN = 2,
|
||||||
TASKKILL = 3,
|
TASKKILL = 3,
|
||||||
BLOCK = 4,
|
BLOCK = 4,
|
||||||
|
RESET = 5,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
31
src/types/folder.ts
Normal file
31
src/types/folder.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
export type Folder = {
|
||||||
|
id: number;
|
||||||
|
deviceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MissingFile = {
|
||||||
|
fileName: string;
|
||||||
|
folderPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExtraFile = {
|
||||||
|
fileName: string;
|
||||||
|
folderPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CurrentFile = {
|
||||||
|
fileName: string;
|
||||||
|
lastModified: string; // ISO date string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClientFolderStatus = {
|
||||||
|
id: number;
|
||||||
|
deviceId: string;
|
||||||
|
missingFiles?: MissingFile[];
|
||||||
|
extraFiles?: ExtraFile[];
|
||||||
|
currentFiles?: CurrentFile[];
|
||||||
|
createdAt: string; // ISO date string
|
||||||
|
updatedAt: string; // ISO date string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClientFolderStatusList = ClientFolderStatus[];
|
||||||
9
src/types/sensitive-command.ts
Normal file
9
src/types/sensitive-command.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import type { CommandType } from "./command-registry";
|
||||||
|
|
||||||
|
export type SensitiveCommand = {
|
||||||
|
commandName: string;
|
||||||
|
commandType: CommandType;
|
||||||
|
commandContent: string;
|
||||||
|
qoS: 0 | 1 | 2;
|
||||||
|
isRetained: boolean;
|
||||||
|
}
|
||||||
11
src/types/user-profile.ts
Normal file
11
src/types/user-profile.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
export type UserProfile = {
|
||||||
|
userName: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
roleId: number;
|
||||||
|
accessRooms: number[];
|
||||||
|
createdAt?: string | null;
|
||||||
|
createdBy?: string | null;
|
||||||
|
updatedAt?: string | null;
|
||||||
|
updatedBy?: string | null;
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user