diff --git a/.idea/TTMT.ManageWebGUI.iml b/.idea/TTMT.ManageWebGUI.iml deleted file mode 100644 index c956989..0000000 --- a/.idea/TTMT.ManageWebGUI.iml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 0b4bf48..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index 77a3198..0000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1772524885874 - - - - - - \ No newline at end of file diff --git a/SERVICES_GUIDE.md b/SERVICES_GUIDE.md deleted file mode 100644 index 5b41a17..0000000 --- a/SERVICES_GUIDE.md +++ /dev/null @@ -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 diff --git a/SERVICES_VS_HOOKS.md b/SERVICES_VS_HOOKS.md deleted file mode 100644 index b5b03cf..0000000 --- a/SERVICES_VS_HOOKS.md +++ /dev/null @@ -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 { - const response = await axios.get( - 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({ - 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
Loading...
- return software?.map(item =>
{item.name}
) -} -``` - ---- - -## 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 { - 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([]); - 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 ( - - ); -} -``` - ---- - -## 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
{/* ... */}
; -} -``` - -### ❌ 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 diff --git a/SYSTEM_ADMIN_PRIORITY_GUIDE.md b/SYSTEM_ADMIN_PRIORITY_GUIDE.md deleted file mode 100644 index 03211d2..0000000 --- a/SYSTEM_ADMIN_PRIORITY_GUIDE.md +++ /dev/null @@ -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 ( -
- {isSystemAdmin() && ( - - )} - -

Role: {role.roleName}

-

Priority: {role.priority}

-
- ); -} -``` - -### 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'; - -{getPriorityLabel(role.priority)} -// 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 ( - - {users.map(user => ( - - {user.name} - - {canDelete(user.role.priority) && ( - - )} - - - ))} -
- ); -} -``` - -### 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! diff --git a/debug-permissions.html b/debug-permissions.html deleted file mode 100644 index dd54f6e..0000000 --- a/debug-permissions.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - Debug Permissions - - - -

Open Browser Console (F12)

-

Or run this in console:

-
-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);
-    
- - diff --git a/src/components/buttons/command-action-buttons.tsx b/src/components/buttons/command-action-buttons.tsx index c865080..569c4b7 100644 --- a/src/components/buttons/command-action-buttons.tsx +++ b/src/components/buttons/command-action-buttons.tsx @@ -14,8 +14,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { useGetCommandsByTypes } from "@/hooks/queries/useCommandQueries"; -import { useSendCommand } from "@/hooks/queries"; +import { useGetSensitiveCommands, useExecuteSensitiveCommand } from "@/hooks/queries/useCommandQueries"; import { CommandType } from "@/types/command-registry"; import { Power, @@ -58,6 +57,12 @@ const COMMAND_TYPE_CONFIG = { color: "text-purple-600", 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) { @@ -65,55 +70,52 @@ export function CommandActionButtons({ roomName, selectedDevices = [] }: Command open: boolean; command: any; commandType: CommandType; + isSensitive?: boolean; }>({ open: false, command: null, commandType: CommandType.RESTART, }); const [isExecuting, setIsExecuting] = useState(false); + const [sensitivePassword, setSensitivePassword] = useState(""); // Query commands for each type - const { data: restartCommands = [] } = useGetCommandsByTypes(CommandType.RESTART.toString()); - const { data: shutdownCommands = [] } = useGetCommandsByTypes(CommandType.SHUTDOWN.toString()); - const { data: taskkillCommands = [] } = useGetCommandsByTypes(CommandType.TASKKILL.toString()); - const { data: blockCommands = [] } = useGetCommandsByTypes(CommandType.BLOCK.toString()); + const { data: sensitiveCommands = [] } = useGetSensitiveCommands(); - // Send command mutation - const sendCommandMutation = useSendCommand(); + // Send command mutation (sensitive) + const executeSensitiveMutation = useExecuteSensitiveCommand(); - const commandsByType = { - [CommandType.RESTART]: restartCommands, - [CommandType.SHUTDOWN]: shutdownCommands, - [CommandType.TASKKILL]: taskkillCommands, - [CommandType.BLOCK]: blockCommands, - }; + // Build commands mapped by CommandType using the `command` field from sensitive data + const commandsByType: Record = (Object.values(CommandType) as Array) + .filter((v) => typeof v === "number") + .reduce((acc: Record, type) => { + acc[type as number] = (sensitiveCommands || []).filter((c: any) => Number(c.command) === Number(type)); + return acc; + }, {} as Record); const handleCommandClick = (command: any, commandType: CommandType) => { + // When building from sensitiveCommands, all items here are sensitive setConfirmDialog({ open: true, command, commandType, + isSensitive: true, }); }; const handleConfirmExecute = async () => { setIsExecuting(true); try { - // Chuẩn bị data theo format API (PascalCase) - const apiData = { - Command: confirmDialog.command.commandContent, - QoS: confirmDialog.command.qoS, - IsRetained: confirmDialog.command.isRetained, - }; - - // Gửi lệnh đến phòng - await sendCommandMutation.mutateAsync({ + // All rendered commands are sourced from sensitiveCommands — send via sensitive mutation + await executeSensitiveMutation.mutateAsync({ roomName, - data: apiData as any, + command: confirmDialog.command.commandContent, + password: sensitivePassword, }); 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 setTimeout(() => { @@ -128,7 +130,8 @@ export function CommandActionButtons({ roomName, selectedDevices = [] }: Command const handleCloseDialog = () => { 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 setTimeout(() => { window.location.reload(); @@ -148,7 +151,7 @@ export function CommandActionButtons({ roomName, selectedDevices = [] }: Command variant="outline" disabled size="sm" - className="gap-2" + className="gap-2 flex-shrink-0" > {config.label} @@ -163,7 +166,7 @@ export function CommandActionButtons({ roomName, selectedDevices = [] }: Command + + {checking && isLoading && ( +
Đang kiểm tra...
+ )} + + {checking && !isLoading && status && ( +
+
Các file trong thư mục Setup({status.currentFiles?.length ?? 0})
+
+ {(status.currentFiles ?? []).length === 0 ? ( +
Không có file hiện tại
+ ) : ( + (status.currentFiles ?? []).map((f: any) => ( +
+
{f.fileName}
+ {f.lastModified && ( +
+ {new Date(f.lastModified).toLocaleString()} +
+ )} +
+ )) + )} +
+
+ )} + + {checking && !isLoading && !status && ( +
Không có dữ liệu
+ )} + + ); + } const DeviceInfo = () => (
@@ -69,6 +127,11 @@ export function ComputerCard({
)} +
+
Kiểm tra thư mục
+ +
+
Trạng thái
{firstNetworkInfo.ipAddress} + {agentVersion && ( +
+ v{agentVersion} +
+ )}
)}
- {isOffline ? ( - - ) : ( - - )} 0; - const hasExtra = status && status.extraFiles.length > 0; + const missing = status?.missingFiles ?? []; + const extra = status?.extraFiles ?? []; + const hasMissing = missing.length > 0; + const hasExtra = extra.length > 0; const hasIssues = hasMissing || hasExtra; // Xác định màu sắc và icon dựa trên trạng thái @@ -80,11 +82,11 @@ export function FolderStatusPopover({

- File thiếu ({status.missingFiles.length}) + File thiếu ({missing.length})

- {status.missingFiles.map((file, idx) => ( + {missing.map((file, idx) => (

- File thừa ({status.extraFiles.length}) + File thừa ({extra.length})

- {status.extraFiles.map((file, idx) => ( + {extra.map((file, idx) => (
+
{title} diff --git a/src/components/grids/device-grid.tsx b/src/components/grids/device-grid.tsx index 04ad38d..b0bcd89 100644 --- a/src/components/grids/device-grid.tsx +++ b/src/components/grids/device-grid.tsx @@ -1,7 +1,7 @@ import { Monitor, DoorOpen } from "lucide-react"; import { ComputerCard } from "../cards/computer-card"; import { useMachineNumber } from "../../hooks/useMachineNumber"; -import type { ClientFolderStatus } from "@/hooks/useClientFolderStatus"; +import type { ClientFolderStatus } from "@/types/folder"; export function DeviceGrid({ devices, diff --git a/src/components/tables/device-table.tsx b/src/components/tables/device-table.tsx index fbc145b..8a6e08e 100644 --- a/src/components/tables/device-table.tsx +++ b/src/components/tables/device-table.tsx @@ -19,11 +19,9 @@ import { Button } from "@/components/ui/button"; import { Wifi, WifiOff, Clock, MapPin, Monitor, ChevronLeft, ChevronRight } from "lucide-react"; import { useMachineNumber } from "@/hooks/useMachineNumber"; import { FolderStatusPopover } from "../folder-status-popover"; -import type { ClientFolderStatus } from "@/hooks/useClientFolderStatus"; interface DeviceTableProps { devices: any[]; - folderStatuses?: Map; isCheckingFolder?: boolean; } @@ -32,7 +30,6 @@ interface DeviceTableProps { */ export function DeviceTable({ devices, - folderStatuses, isCheckingFolder, }: DeviceTableProps) { const getMachineNumber = useMachineNumber(); @@ -151,7 +148,6 @@ export function DeviceTable({ const device = row.original; const isOffline = device.isOffline; const macAddress = device.networkInfos?.[0]?.macAddress || device.id; - const folderStatus = folderStatuses?.get(macAddress); if (isOffline) { return -; @@ -160,7 +156,6 @@ export function DeviceTable({ return ( ); diff --git a/src/config/api.ts b/src/config/api.ts index fd6cc56..6af1583 100644 --- a/src/config/api.ts +++ b/src/config/api.ts @@ -13,6 +13,7 @@ export const API_ENDPOINTS = { PING: `${BASE_URL}/ping`, CSRF_TOKEN: `${BASE_URL}/csrf-token`, CREATE_ACCOUNT: `${BASE_URL}/auth/create-account`, + GET_USERS_LIST: `${BASE_URL}/users-info`, }, APP_VERSION: { //agent and app api @@ -45,7 +46,7 @@ export const API_ENDPOINTS = { SEND_COMMAND: (roomName: string) => `${BASE_URL}/DeviceComm/shellcommand/${roomName}`, CHANGE_DEVICE_ROOM: `${BASE_URL}/DeviceComm/changeroom`, REQUEST_GET_CLIENT_FOLDER_STATUS: (roomName: string) => - `${BASE_URL}/DeviceComm/clientfolderstatus/${roomName}`, + `${BASE_URL}/DeviceComm/folderstatuses/${roomName}`, }, COMMAND: { 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}`, UPDATE_COMMAND: (commandId: number) => `${BASE_URL}/Command/update/${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: { DEVICE_ONLINE: `${BASE_URL}/Sse/events/onlineDevices`, diff --git a/src/hooks/queries/index.ts b/src/hooks/queries/index.ts index 490d480..3804d03 100644 --- a/src/hooks/queries/index.ts +++ b/src/hooks/queries/index.ts @@ -15,3 +15,5 @@ export * from "./usePermissionQueries"; // Role Queries export * from "./useRoleQueries"; +// User Queries +export * from "./useUserQueries"; diff --git a/src/hooks/queries/useCommandQueries.ts b/src/hooks/queries/useCommandQueries.ts index f90e1d2..3bdb20c 100644 --- a/src/hooks/queries/useCommandQueries.ts +++ b/src/hooks/queries/useCommandQueries.ts @@ -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"], + }); + }, + }); +} \ No newline at end of file diff --git a/src/hooks/queries/useDeviceCommQueries.ts b/src/hooks/queries/useDeviceCommQueries.ts index f938f01..d4086d7 100644 --- a/src/hooks/queries/useDeviceCommQueries.ts +++ b/src/hooks/queries/useDeviceCommQueries.ts @@ -1,6 +1,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import * as deviceCommService from "@/services/device-comm.service"; import type { DeviceHealthCheck } from "@/types/device"; +import type { ClientFolderStatus } from "@/types/folder"; const DEVICE_COMM_QUERY_KEYS = { all: ["device-comm"] as const, @@ -158,7 +159,7 @@ export function useChangeDeviceRoom() { * Hook để lấy trạng thái folder client */ export function useGetClientFolderStatus(roomName?: string, enabled = true) { - return useQuery({ + return useQuery({ queryKey: roomName ? DEVICE_COMM_QUERY_KEYS.clientFolderStatus(roomName) : ["disabled"], @@ -170,3 +171,28 @@ export function useGetClientFolderStatus(roomName?: string, enabled = true) { 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({ + 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, + }); +} diff --git a/src/hooks/queries/useUserQueries.ts b/src/hooks/queries/useUserQueries.ts new file mode 100644 index 0000000..4592649 --- /dev/null +++ b/src/hooks/queries/useUserQueries.ts @@ -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({ + queryKey: USER_QUERY_KEYS.list(), + queryFn: () => userService.getUsersInfo(), + enabled, + staleTime: 5 * 60 * 1000, // 5 minutes + }); +} diff --git a/src/hooks/useClientFolderStatus.ts b/src/hooks/useClientFolderStatus.ts deleted file mode 100644 index 3ec0bec..0000000 --- a/src/hooks/useClientFolderStatus.ts +++ /dev/null @@ -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 | null>(null); - const [folderStatuses, setFolderStatuses] = useState< - Map - >(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; -} diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index b7464ab..1cdba4b 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -11,6 +11,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as AuthRouteImport } from './routes/_auth' 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 AuthRoleIndexRouteImport } from './routes/_auth/role/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 AuthAgentIndexRouteImport } from './routes/_auth/agent/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 AuthRoomsRoomNameIndexRouteImport } from './routes/_auth/rooms/$roomName/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 AuthUserRoleRoleIdIndexRouteImport } from './routes/_auth/user/role/$roleId/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' const AuthRoute = AuthRouteImport.update({ @@ -39,6 +40,11 @@ const IndexRoute = IndexRouteImport.update({ path: '/', getParentRoute: () => rootRouteImport, } as any) +const AuthUserIndexRoute = AuthUserIndexRouteImport.update({ + id: '/user/', + path: '/user/', + getParentRoute: () => AuthRoute, +} as any) const AuthRoomsIndexRoute = AuthRoomsIndexRouteImport.update({ id: '/rooms/', path: '/rooms/', @@ -84,11 +90,6 @@ const authLoginIndexRoute = authLoginIndexRouteImport.update({ path: '/login/', getParentRoute: () => rootRouteImport, } as any) -const AuthUserRoleIndexRoute = AuthUserRoleIndexRouteImport.update({ - id: '/user/role/', - path: '/user/role/', - getParentRoute: () => AuthRoute, -} as any) const AuthUserCreateIndexRoute = AuthUserCreateIndexRouteImport.update({ id: '/user/create/', path: '/user/create/', @@ -127,6 +128,12 @@ const AuthUserChangePasswordUserNameIndexRoute = path: '/user/change-password/$userName/', getParentRoute: () => AuthRoute, } as any) +const AuthRoomsRoomNameFolderStatusIndexRoute = + AuthRoomsRoomNameFolderStatusIndexRouteImport.update({ + id: '/rooms/$roomName/folder-status/', + path: '/rooms/$roomName/folder-status/', + getParentRoute: () => AuthRoute, + } as any) const AuthRoleIdEditIndexRoute = AuthRoleIdEditIndexRouteImport.update({ id: '/role/$id/edit/', path: '/role/$id/edit/', @@ -144,13 +151,14 @@ export interface FileRoutesByFullPath { '/device': typeof AuthDeviceIndexRoute '/role': typeof AuthRoleIndexRoute '/rooms': typeof AuthRoomsIndexRoute + '/user': typeof AuthUserIndexRoute '/profile/$userName': typeof AuthProfileUserNameIndexRoute '/profile/change-password': typeof AuthProfileChangePasswordIndexRoute '/role/create': typeof AuthRoleCreateIndexRoute '/rooms/$roomName': typeof AuthRoomsRoomNameIndexRoute '/user/create': typeof AuthUserCreateIndexRoute - '/user/role': typeof AuthUserRoleIndexRoute '/role/$id/edit': typeof AuthRoleIdEditIndexRoute + '/rooms/$roomName/folder-status': typeof AuthRoomsRoomNameFolderStatusIndexRoute '/user/change-password/$userName': typeof AuthUserChangePasswordUserNameIndexRoute '/user/role/$roleId': typeof AuthUserRoleRoleIdIndexRoute } @@ -165,13 +173,14 @@ export interface FileRoutesByTo { '/device': typeof AuthDeviceIndexRoute '/role': typeof AuthRoleIndexRoute '/rooms': typeof AuthRoomsIndexRoute + '/user': typeof AuthUserIndexRoute '/profile/$userName': typeof AuthProfileUserNameIndexRoute '/profile/change-password': typeof AuthProfileChangePasswordIndexRoute '/role/create': typeof AuthRoleCreateIndexRoute '/rooms/$roomName': typeof AuthRoomsRoomNameIndexRoute '/user/create': typeof AuthUserCreateIndexRoute - '/user/role': typeof AuthUserRoleIndexRoute '/role/$id/edit': typeof AuthRoleIdEditIndexRoute + '/rooms/$roomName/folder-status': typeof AuthRoomsRoomNameFolderStatusIndexRoute '/user/change-password/$userName': typeof AuthUserChangePasswordUserNameIndexRoute '/user/role/$roleId': typeof AuthUserRoleRoleIdIndexRoute } @@ -188,13 +197,14 @@ export interface FileRoutesById { '/_auth/device/': typeof AuthDeviceIndexRoute '/_auth/role/': typeof AuthRoleIndexRoute '/_auth/rooms/': typeof AuthRoomsIndexRoute + '/_auth/user/': typeof AuthUserIndexRoute '/_auth/profile/$userName/': typeof AuthProfileUserNameIndexRoute '/_auth/profile/change-password/': typeof AuthProfileChangePasswordIndexRoute '/_auth/role/create/': typeof AuthRoleCreateIndexRoute '/_auth/rooms/$roomName/': typeof AuthRoomsRoomNameIndexRoute '/_auth/user/create/': typeof AuthUserCreateIndexRoute - '/_auth/user/role/': typeof AuthUserRoleIndexRoute '/_auth/role/$id/edit/': typeof AuthRoleIdEditIndexRoute + '/_auth/rooms/$roomName/folder-status/': typeof AuthRoomsRoomNameFolderStatusIndexRoute '/_auth/user/change-password/$userName/': typeof AuthUserChangePasswordUserNameIndexRoute '/_auth/user/role/$roleId/': typeof AuthUserRoleRoleIdIndexRoute } @@ -211,13 +221,14 @@ export interface FileRouteTypes { | '/device' | '/role' | '/rooms' + | '/user' | '/profile/$userName' | '/profile/change-password' | '/role/create' | '/rooms/$roomName' | '/user/create' - | '/user/role' | '/role/$id/edit' + | '/rooms/$roomName/folder-status' | '/user/change-password/$userName' | '/user/role/$roleId' fileRoutesByTo: FileRoutesByTo @@ -232,13 +243,14 @@ export interface FileRouteTypes { | '/device' | '/role' | '/rooms' + | '/user' | '/profile/$userName' | '/profile/change-password' | '/role/create' | '/rooms/$roomName' | '/user/create' - | '/user/role' | '/role/$id/edit' + | '/rooms/$roomName/folder-status' | '/user/change-password/$userName' | '/user/role/$roleId' id: @@ -254,13 +266,14 @@ export interface FileRouteTypes { | '/_auth/device/' | '/_auth/role/' | '/_auth/rooms/' + | '/_auth/user/' | '/_auth/profile/$userName/' | '/_auth/profile/change-password/' | '/_auth/role/create/' | '/_auth/rooms/$roomName/' | '/_auth/user/create/' - | '/_auth/user/role/' | '/_auth/role/$id/edit/' + | '/_auth/rooms/$roomName/folder-status/' | '/_auth/user/change-password/$userName/' | '/_auth/user/role/$roleId/' fileRoutesById: FileRoutesById @@ -287,6 +300,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } + '/_auth/user/': { + id: '/_auth/user/' + path: '/user' + fullPath: '/user' + preLoaderRoute: typeof AuthUserIndexRouteImport + parentRoute: typeof AuthRoute + } '/_auth/rooms/': { id: '/_auth/rooms/' path: '/rooms' @@ -350,13 +370,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof authLoginIndexRouteImport 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/': { id: '/_auth/user/create/' path: '/user/create' @@ -406,6 +419,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthUserChangePasswordUserNameIndexRouteImport 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/': { id: '/_auth/role/$id/edit/' path: '/role/$id/edit' @@ -425,13 +445,14 @@ interface AuthRouteChildren { AuthDeviceIndexRoute: typeof AuthDeviceIndexRoute AuthRoleIndexRoute: typeof AuthRoleIndexRoute AuthRoomsIndexRoute: typeof AuthRoomsIndexRoute + AuthUserIndexRoute: typeof AuthUserIndexRoute AuthProfileUserNameIndexRoute: typeof AuthProfileUserNameIndexRoute AuthProfileChangePasswordIndexRoute: typeof AuthProfileChangePasswordIndexRoute AuthRoleCreateIndexRoute: typeof AuthRoleCreateIndexRoute AuthRoomsRoomNameIndexRoute: typeof AuthRoomsRoomNameIndexRoute AuthUserCreateIndexRoute: typeof AuthUserCreateIndexRoute - AuthUserRoleIndexRoute: typeof AuthUserRoleIndexRoute AuthRoleIdEditIndexRoute: typeof AuthRoleIdEditIndexRoute + AuthRoomsRoomNameFolderStatusIndexRoute: typeof AuthRoomsRoomNameFolderStatusIndexRoute AuthUserChangePasswordUserNameIndexRoute: typeof AuthUserChangePasswordUserNameIndexRoute AuthUserRoleRoleIdIndexRoute: typeof AuthUserRoleRoleIdIndexRoute } @@ -445,13 +466,15 @@ const AuthRouteChildren: AuthRouteChildren = { AuthDeviceIndexRoute: AuthDeviceIndexRoute, AuthRoleIndexRoute: AuthRoleIndexRoute, AuthRoomsIndexRoute: AuthRoomsIndexRoute, + AuthUserIndexRoute: AuthUserIndexRoute, AuthProfileUserNameIndexRoute: AuthProfileUserNameIndexRoute, AuthProfileChangePasswordIndexRoute: AuthProfileChangePasswordIndexRoute, AuthRoleCreateIndexRoute: AuthRoleCreateIndexRoute, AuthRoomsRoomNameIndexRoute: AuthRoomsRoomNameIndexRoute, AuthUserCreateIndexRoute: AuthUserCreateIndexRoute, - AuthUserRoleIndexRoute: AuthUserRoleIndexRoute, AuthRoleIdEditIndexRoute: AuthRoleIdEditIndexRoute, + AuthRoomsRoomNameFolderStatusIndexRoute: + AuthRoomsRoomNameFolderStatusIndexRoute, AuthUserChangePasswordUserNameIndexRoute: AuthUserChangePasswordUserNameIndexRoute, AuthUserRoleRoleIdIndexRoute: AuthUserRoleRoleIdIndexRoute, diff --git a/src/routes/_auth/agent/index.tsx b/src/routes/_auth/agent/index.tsx index f6442fb..ba9d54e 100644 --- a/src/routes/_auth/agent/index.tsx +++ b/src/routes/_auth/agent/index.tsx @@ -74,7 +74,6 @@ function AgentsPage() { const columns: ColumnDef[] = [ { accessorKey: "version", header: "Phiên bản" }, { accessorKey: "fileName", header: "Tên file" }, - { accessorKey: "folderPath", header: "Đường dẫn" }, { accessorKey: "updatedAt", header: "Thời gian cập nhật", diff --git a/src/routes/_auth/apps/index.tsx b/src/routes/_auth/apps/index.tsx index 2d655da..8ae6fe6 100644 --- a/src/routes/_auth/apps/index.tsx +++ b/src/routes/_auth/apps/index.tsx @@ -55,7 +55,6 @@ function AppsComponent() { const columns: ColumnDef[] = [ { accessorKey: "version", header: "Phiên bản" }, { accessorKey: "fileName", header: "Tên file" }, - { accessorKey: "folderPath", header: "Đường dẫn" }, { accessorKey: "updatedAt", header: () =>
Thời gian cập nhật
, diff --git a/src/routes/_auth/commands/index.tsx b/src/routes/_auth/commands/index.tsx index 58713a8..86950e6 100644 --- a/src/routes/_auth/commands/index.tsx +++ b/src/routes/_auth/commands/index.tsx @@ -11,9 +11,9 @@ import { useSendCommand, } from "@/hooks/queries"; import { toast } from "sonner"; +import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; import { Check, X, Edit2, Trash2 } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import type { ColumnDef } from "@tanstack/react-table"; import type { ShellCommandData } from "@/components/forms/command-form"; 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" }] }), component: CommandPage, 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 = [ { 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() { const [isDialogOpen, setIsDialogOpen] = useState(false); const [selectedCommand, setSelectedCommand] = useState(null); - const [detailPanelCommand, setDetailPanelCommand] = useState(null); const [table, setTable] = useState(); // Fetch commands @@ -58,13 +72,16 @@ function CommandPage() { const columns: ColumnDef[] = [ { accessorKey: "commandName", - header: "Tên lệnh", + header: () =>
Tên lệnh
, size: 100, - cell: ({ getValue }) => ( -
- {getValue() as string} -
- ), + cell: ({ getValue, row }) => { + const full = (getValue() as string) || row.original.commandName || ""; + return ( +
+ {full} +
+ ); + }, }, { accessorKey: "commandType", @@ -80,18 +97,6 @@ function CommandPage() { return {typeMap[type] || "UNKNOWN"}; }, }, - { - accessorKey: "description", - header: "Mô tả", - size: 120, - cell: ({ getValue }) => ( -
- - {(getValue() as string) || "-"} - -
- ), - }, { accessorKey: "commandContent", header: "Nội dung lệnh", @@ -140,7 +145,7 @@ function CommandPage() { }, { id: "select", - header: () =>
Chọn để thực thi
, + header: () =>
Thực thi
, cell: ({ row }) => ( setDetailPanelCommand(row)} scrollable={true} maxHeight="500px" - enablePagination + enablePagination={false} defaultPageSize={10} /> - - {/* Detail Dialog Popup */} - !open && setDetailPanelCommand(null)}> - - - Chi tiết lệnh - - - {detailPanelCommand && ( -
- {/* Command Name */} -
-

Tên lệnh

-

{detailPanelCommand.commandName}

-
- - {/* Command Type */} -
-

Loại lệnh

-

- { - { - 1: "RESTART", - 2: "SHUTDOWN", - 3: "TASKKILL", - 4: "BLOCK", - }[detailPanelCommand.commandType] || "UNKNOWN" - } -

-
- - {/* Description */} -
-

Mô tả

-

- {detailPanelCommand.description || "-"} -

-
- - {/* Command Content */} -
-

Nội dung lệnh

-
- - {detailPanelCommand.commandContent} - -
-
- - {/* QoS */} -
-

QoS

-

- - {detailPanelCommand.qoS ?? 0} - -

-
- - {/* Retention */} -
-

Lưu trữ

-
- {detailPanelCommand.isRetained ? ( - <> - - - - ) : ( - <> - - Không - - )} -
-
-
- )} -
-
); } diff --git a/src/routes/_auth/rooms/$roomName/folder-status/index.tsx b/src/routes/_auth/rooms/$roomName/folder-status/index.tsx new file mode 100644 index 0000000..d7fca40 --- /dev/null +++ b/src/routes/_auth/rooms/$roomName/folder-status/index.tsx @@ -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(); + + 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 ( + + navigate({ to: "/rooms/$roomName/", params: { roomName } } as any) + } + table={table} + /> + ); +} diff --git a/src/routes/_auth/rooms/$roomName/index.tsx b/src/routes/_auth/rooms/$roomName/index.tsx index aa9f014..738a90c 100644 --- a/src/routes/_auth/rooms/$roomName/index.tsx +++ b/src/routes/_auth/rooms/$roomName/index.tsx @@ -1,62 +1,41 @@ -import { createFileRoute, useParams } from "@tanstack/react-router"; +import { createFileRoute, useParams, useNavigate } from "@tanstack/react-router"; import { useState } from "react"; 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 { useGetDeviceFromRoom } from "@/hooks/queries"; import { useDeviceEvents } from "@/hooks/useDeviceEvents"; -import { useClientFolderStatus } from "@/hooks/useClientFolderStatus"; import { DeviceGrid } from "@/components/grids/device-grid"; import { DeviceTable } from "@/components/tables/device-table"; import { useMachineNumber } from "@/hooks/useMachineNumber"; -import { toast } from "sonner"; import { CommandActionButtons } from "@/components/buttons/command-action-buttons"; export const Route = createFileRoute("/_auth/rooms/$roomName/")({ head: ({ params }) => ({ 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, }); function RoomDetailPage() { const { roomName } = useParams({ from: "/_auth/rooms/$roomName/" }); const [viewMode, setViewMode] = useState<"grid" | "table">("grid"); - const [isCheckingFolder, setIsCheckingFolder] = useState(false); // SSE real-time updates useDeviceEvents(roomName); - // Folder status from SSE - const folderStatuses = useClientFolderStatus(roomName); - + // Folder status from SS const { data: devices = [] } = useGetDeviceFromRoom(roomName); const parseMachineNumber = useMachineNumber(); - const handleCheckFolderStatus = async () => { - 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 navigate = useNavigate(); const sortedDevices = [...devices].sort((a, b) => { return parseMachineNumber(a.id) - parseMachineNumber(b.id); @@ -101,7 +80,7 @@ function RoomDetailPage() { Thực thi lệnh
-
+
{/* Command Action Buttons */} {devices.length > 0 && ( <> @@ -110,18 +89,18 @@ function RoomDetailPage() {
)} @@ -141,18 +120,15 @@ function RoomDetailPage() { ) : viewMode === "grid" ? ( ) : ( )} +
); } diff --git a/src/routes/_auth/rooms/index.tsx b/src/routes/_auth/rooms/index.tsx index 698aa02..50b7f04 100644 --- a/src/routes/_auth/rooms/index.tsx +++ b/src/routes/_auth/rooms/index.tsx @@ -35,6 +35,9 @@ export const Route = createFileRoute("/_auth/rooms/")({ head: () => ({ meta: [{ title: "Danh sách phòng" }], }), + loader: async ({ context }) => { + context.breadcrumbs = [{ title: "Danh sách phòng", path: "#" }]; + }, component: RoomComponent, }); diff --git a/src/routes/_auth/user/create/index.tsx b/src/routes/_auth/user/create/index.tsx index 7cdf745..2470107 100644 --- a/src/routes/_auth/user/create/index.tsx +++ b/src/routes/_auth/user/create/index.tsx @@ -135,7 +135,7 @@ function CreateUserComponent() {
+ + +
+ ), + enableSorting: false, + enableHiding: false, + }, + ], + [navigate], + ); + + const tableData = users.map((u) => ({ ...u, id: u.userName })); + + return ( + + +
+ } + > +
+ []} + scrollable={true} + maxHeight="400px" + enablePagination={false} + onTableInit={(t) => setTable(t)} + /> +
+ + ); +} diff --git a/src/routes/_auth/user/role/$roleId/index.tsx b/src/routes/_auth/user/role/$roleId/index.tsx index 0e2e0a1..b7521e6 100644 --- a/src/routes/_auth/user/role/$roleId/index.tsx +++ b/src/routes/_auth/user/role/$roleId/index.tsx @@ -13,11 +13,24 @@ import { Shield, ArrowLeft, Check, X } from "lucide-react"; import type { PermissionOnRole } from "@/types/permission"; export const Route = createFileRoute("/_auth/user/role/$roleId/")({ + head: () => ({ + meta: [{ title: "Quyền của người dùng | AccessControl" }] + }), component: ViewRolePermissionsComponent, loader: async ({ context, params }) => { 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 (
- @@ -88,11 +101,11 @@ function ViewRolePermissionsComponent() {
- {parent?.permissionName || "Unknown"} + {parent?.permissionName || "Allow all"} {parent?.permissionCode}
- {parent?.isChecked === 1 ? ( + {(parent?.isChecked === 1 || parent === null) ? ( Đã bật diff --git a/src/routes/_auth/user/role/index.tsx b/src/routes/_auth/user/role/index.tsx deleted file mode 100644 index f720d09..0000000 --- a/src/routes/_auth/user/role/index.tsx +++ /dev/null @@ -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 ( -
- -
- ); - } - - if (isError) { - return ( -
- - -
- Không thể tải danh sách vai trò -
-
- -
-
-
-
- ); - } - - return ( -
-
- -
- - - - - - Danh sách vai trò - - - Quản lý các vai trò và quyền hạn trong hệ thống - - - - {roles && roles.length > 0 ? ( - - - - ID - Tên vai trò - Độ ưu tiên - Ngày tạo - Thao tác - - - - {roles.map((role) => ( - - - {role.id} - - {role.roleName} - - {role.priority} - - - {role.createdAt ? new Date(role.createdAt).toLocaleDateString("vi-VN") : "—"} - - - - - - ))} - -
- ) : ( -
- Không có vai trò nào trong hệ thống -
- )} -
-
-
- ); -} diff --git a/src/services/app-version.service.ts b/src/services/app-version.service.ts index 5677c32..a8b936a 100644 --- a/src/services/app-version.service.ts +++ b/src/services/app-version.service.ts @@ -111,7 +111,7 @@ export async function addRequiredFile(data: any): Promise<{ message: string }> { * @param fileId - ID file */ 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) ); return response.data; diff --git a/src/services/command.service.ts b/src/services/command.service.ts index 325641b..0d63b5e 100644 --- a/src/services/command.service.ts +++ b/src/services/command.service.ts @@ -51,3 +51,22 @@ export async function deleteCommand(commandId: number): Promise { ); 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 { + const response = await axios.get(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 { + const response = await axios.post(API_ENDPOINTS.COMMAND.REQUEST_SEND_SENSITIVE_COMMAND, data); + return response.data; +} diff --git a/src/services/user.service.ts b/src/services/user.service.ts new file mode 100644 index 0000000..e196400 --- /dev/null +++ b/src/services/user.service.ts @@ -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 { + try { + const response = await axios.get(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 }; diff --git a/src/template/command-submit-template.tsx b/src/template/command-submit-template.tsx index 9d46e68..3484bc2 100644 --- a/src/template/command-submit-template.tsx +++ b/src/template/command-submit-template.tsx @@ -96,12 +96,28 @@ export function CommandSubmitTemplate({ pageSizeOptions = [5, 10, 15, 20], }: CommandSubmitTemplateProps) { 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 [customQoS, setCustomQoS] = useState<0 | 1 | 2>(0); const [customRetained, setCustomRetained] = useState(false); const [table, setTable] = useState(); 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) => { setTable(t); @@ -141,7 +157,7 @@ export function CommandSubmitTemplate({ if (!onExecuteSelected) return; try { const roomNames = rooms.map((room) => - typeof room === "string" ? room : room.name + typeof room === "string" ? room : room.name, ); const allTargets = [...roomNames, ...devices]; onExecuteSelected(allTargets); @@ -176,7 +192,7 @@ export function CommandSubmitTemplate({ if (!onExecuteCustom) return; try { const roomNames = rooms.map((room) => - typeof room === "string" ? room : room.name + typeof room === "string" ? room : room.name, ); const allTargets = [...roomNames, ...devices]; handleExecuteCustom(allTargets); @@ -227,7 +243,7 @@ export function CommandSubmitTemplate({ {/* Tabs Navigation */}
{/* Tab 1: Danh sách */} @@ -432,11 +448,16 @@ export function CommandSubmitTemplate({ }} /> )} - + {/* Dialog for add/edit */} {formContent && ( - + {dialogTitle} diff --git a/src/template/folder-status-template.tsx b/src/template/folder-status-template.tsx new file mode 100644 index 0000000..6fae782 --- /dev/null +++ b/src/template/folder-status-template.tsx @@ -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(null); + // If a table instance is provided (pre-built), use it; otherwise render simple table + if (table) { + return ( +
+
+
+ +

Kết quả kiểm tra thư mục Setup — {roomName}

+
+
+ +
+ + + {table.getHeaderGroups().map((hg: any) => ( + + {hg.headers.map((h: any) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row: any) => ( + + setExpandedId((prev) => (prev === row.id ? null : row.id))} + className="hover:bg-muted/20 cursor-pointer" + > + {row.getVisibleCells().map((cell: any) => ( + + ))} + + + {/* Detail row - shown when expanded */} + {expandedId === row.id && ( + + + + )} + + ))} + +
+ {flexRender(h.column.columnDef.header, h.getContext())} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+
+
Các file đang có trong thư mục ({row.original.currentFiles?.length ?? 0})
+ {(row.original.currentFiles ?? []).map((f: CurrentFile) => ( +
{f.fileName}
+ ))} +
+
+
+
+
+ ); + } + + // Fallback simple list view if table not provided + return ( +
+
+
+ +

Kết quả kiểm tra thư mục Setup — {roomName}

+
+
+ +
+ {isLoading ? ( +
Đang tải...
+ ) : data.length === 0 ? ( +
Không có dữ liệu
+ ) : ( +
+ {data.map((item) => ( +
+
+
{item.deviceId}
+
{new Date(item.updatedAt).toLocaleString("vi-VN")}
+
+ +
+
+
Các file đang có trong thư mục ({item.currentFiles?.length ?? 0})
+ {(item.currentFiles ?? []).map((f: CurrentFile) => ( +
{f.fileName}
+ ))} +
+
+
+ ))} +
+ )} +
+
+ ); +} diff --git a/src/template/user-list-template.tsx b/src/template/user-list-template.tsx new file mode 100644 index 0000000..1912e4f --- /dev/null +++ b/src/template/user-list-template.tsx @@ -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 ( +
+
+
+

{title}

+ {description &&
{description}
} +
+
{headerAction}
+
+ + + +
+ Danh sách người dùng + {description && {description}} +
+
+ + {children} + {footer && {footer}} +
+
+ ); +} + +export default UserListTemplate; diff --git a/src/types/app-sidebar.ts b/src/types/app-sidebar.ts index 027da65..8adc1eb 100644 --- a/src/types/app-sidebar.ts +++ b/src/types/app-sidebar.ts @@ -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"; enum AppSidebarSectionCode { @@ -18,10 +18,10 @@ export const appSidebarSection = { versions: ["1.0.1", "1.1.0-alpha", "2.0.0-beta1"], navMain: [ { - title: "Thống kê tổng quan", + title: "Tổng quan", items: [ { - title: "Dashboard", + title: "Thống kê", url: "/dashboard", code: AppSidebarSectionCode.DASHBOARD, 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: [ { 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: [ { - title: "Danh sách Agent", + title: "Agent", url: "/agent", code: AppSidebarSectionCode.AGENT_MANAGEMENT, icon: AppWindow, permissions: [PermissionEnum.VIEW_AGENT], }, { - title: "Quản lý phần mềm", + title: "Thư mục Setup", url: "/apps", - icon: AppWindow, + icon: Folder, permissions: [PermissionEnum.VIEW_APPS], } ], }, { - title: "Lệnh và các ứng dụng bị chặn", + title: "Quản lý lệnh/blacklist", items: [ { @@ -70,7 +70,7 @@ export const appSidebarSection = { permissions: [PermissionEnum.VIEW_COMMAND], }, { - title: "Danh sách ứng dụng/web bị chặn", + title: "Chặn ứng dụng/website", url: "/blacklists", icon: CircleX, 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: [ { title: "Danh sách roles", @@ -87,10 +87,10 @@ export const appSidebarSection = { permissions: [PermissionEnum.VIEW_ROLES], }, { - title: "Tạo người dùng", - url: "/user/create", + title: "Danh sách người dùng", + url: "/user", icon: UserPlus, - permissions: [PermissionEnum.CRE_USER], + permissions: [PermissionEnum.VIEW_USER], } ] } diff --git a/src/types/command-registry.ts b/src/types/command-registry.ts index fbfc4d2..aa9445a 100644 --- a/src/types/command-registry.ts +++ b/src/types/command-registry.ts @@ -15,4 +15,5 @@ export enum CommandType { SHUTDOWN = 2, TASKKILL = 3, BLOCK = 4, + RESET = 5, } diff --git a/src/types/folder.ts b/src/types/folder.ts new file mode 100644 index 0000000..d20956a --- /dev/null +++ b/src/types/folder.ts @@ -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[]; \ No newline at end of file diff --git a/src/types/sensitive-command.ts b/src/types/sensitive-command.ts new file mode 100644 index 0000000..1d04ed6 --- /dev/null +++ b/src/types/sensitive-command.ts @@ -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; +} \ No newline at end of file diff --git a/src/types/user-profile.ts b/src/types/user-profile.ts new file mode 100644 index 0000000..e561b28 --- /dev/null +++ b/src/types/user-profile.ts @@ -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; +}; \ No newline at end of file