From 67f5dbbb08dd8829ef0768a7036c3e41b74772a1 Mon Sep 17 00:00:00 2001 From: phuongdm Date: Wed, 18 Mar 2026 13:58:59 +0700 Subject: [PATCH] fix UX and add check Setup folder route --- SERVICES_GUIDE.md | 174 ---------- SERVICES_VS_HOOKS.md | 328 ------------------ SYSTEM_ADMIN_PRIORITY_GUIDE.md | 249 ------------- debug-permissions.html | 26 -- src/components/cards/computer-card.tsx | 14 +- src/components/folder-status-popover.tsx | 16 +- .../forms/command-registry-form.tsx | 2 +- src/components/grids/device-grid.tsx | 2 +- src/components/tables/device-table.tsx | 5 - src/config/api.ts | 2 +- src/hooks/queries/useDeviceCommQueries.ts | 3 +- src/hooks/useClientFolderStatus.ts | 83 ----- src/routeTree.gen.ts | 44 +++ src/routes/_auth/commands/index.tsx | 110 +----- .../rooms/$roomName/folder-status/index.tsx | 99 ++++++ src/routes/_auth/rooms/$roomName/index.tsx | 62 +--- src/routes/_auth/rooms/index.tsx | 3 + src/routes/_auth/user/index.tsx | 9 + src/template/command-submit-template.tsx | 39 ++- src/template/folder-status-template.tsx | 124 +++++++ src/types/app-sidebar.ts | 6 +- src/types/folder.ts | 31 ++ 22 files changed, 400 insertions(+), 1031 deletions(-) delete mode 100644 SERVICES_GUIDE.md delete mode 100644 SERVICES_VS_HOOKS.md delete mode 100644 SYSTEM_ADMIN_PRIORITY_GUIDE.md delete mode 100644 debug-permissions.html delete mode 100644 src/hooks/useClientFolderStatus.ts create mode 100644 src/routes/_auth/rooms/$roomName/folder-status/index.tsx create mode 100644 src/routes/_auth/user/index.tsx create mode 100644 src/template/folder-status-template.tsx create mode 100644 src/types/folder.ts 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/cards/computer-card.tsx b/src/components/cards/computer-card.tsx index e1a1d5b..c271450 100644 --- a/src/components/cards/computer-card.tsx +++ b/src/components/cards/computer-card.tsx @@ -3,8 +3,7 @@ import { Badge } from "@/components/ui/badge"; import { Monitor, Wifi, WifiOff } from "lucide-react"; import { cn } from "@/lib/utils"; import { FolderStatusPopover } from "../folder-status-popover"; -import type { ClientFolderStatus } from "@/hooks/useClientFolderStatus"; - +import type { ClientFolderStatus } from "@/types/folder"; export function ComputerCard({ device, position, @@ -30,6 +29,7 @@ export function ComputerCard({ const isOffline = device.isOffline; const firstNetworkInfo = device.networkInfos?.[0]; + const agentVersion = device.version; const DeviceInfo = () => (
@@ -119,14 +119,14 @@ export function ComputerCard({ {firstNetworkInfo?.ipAddress && (
{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..400e20a 100644 --- a/src/config/api.ts +++ b/src/config/api.ts @@ -45,7 +45,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`, diff --git a/src/hooks/queries/useDeviceCommQueries.ts b/src/hooks/queries/useDeviceCommQueries.ts index f938f01..d1f8d8c 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"], 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..1af80a4 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' @@ -28,6 +29,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 +41,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/', @@ -127,6 +134,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,6 +157,7 @@ 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 @@ -151,6 +165,7 @@ export interface FileRoutesByFullPath { '/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,6 +180,7 @@ 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 @@ -172,6 +188,7 @@ export interface FileRoutesByTo { '/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,6 +205,7 @@ 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 @@ -195,6 +213,7 @@ export interface FileRoutesById { '/_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,6 +230,7 @@ export interface FileRouteTypes { | '/device' | '/role' | '/rooms' + | '/user' | '/profile/$userName' | '/profile/change-password' | '/role/create' @@ -218,6 +238,7 @@ export interface FileRouteTypes { | '/user/create' | '/user/role' | '/role/$id/edit' + | '/rooms/$roomName/folder-status' | '/user/change-password/$userName' | '/user/role/$roleId' fileRoutesByTo: FileRoutesByTo @@ -232,6 +253,7 @@ export interface FileRouteTypes { | '/device' | '/role' | '/rooms' + | '/user' | '/profile/$userName' | '/profile/change-password' | '/role/create' @@ -239,6 +261,7 @@ export interface FileRouteTypes { | '/user/create' | '/user/role' | '/role/$id/edit' + | '/rooms/$roomName/folder-status' | '/user/change-password/$userName' | '/user/role/$roleId' id: @@ -254,6 +277,7 @@ export interface FileRouteTypes { | '/_auth/device/' | '/_auth/role/' | '/_auth/rooms/' + | '/_auth/user/' | '/_auth/profile/$userName/' | '/_auth/profile/change-password/' | '/_auth/role/create/' @@ -261,6 +285,7 @@ export interface FileRouteTypes { | '/_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 +312,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' @@ -406,6 +438,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,6 +464,7 @@ interface AuthRouteChildren { AuthDeviceIndexRoute: typeof AuthDeviceIndexRoute AuthRoleIndexRoute: typeof AuthRoleIndexRoute AuthRoomsIndexRoute: typeof AuthRoomsIndexRoute + AuthUserIndexRoute: typeof AuthUserIndexRoute AuthProfileUserNameIndexRoute: typeof AuthProfileUserNameIndexRoute AuthProfileChangePasswordIndexRoute: typeof AuthProfileChangePasswordIndexRoute AuthRoleCreateIndexRoute: typeof AuthRoleCreateIndexRoute @@ -432,6 +472,7 @@ interface AuthRouteChildren { AuthUserCreateIndexRoute: typeof AuthUserCreateIndexRoute AuthUserRoleIndexRoute: typeof AuthUserRoleIndexRoute AuthRoleIdEditIndexRoute: typeof AuthRoleIdEditIndexRoute + AuthRoomsRoomNameFolderStatusIndexRoute: typeof AuthRoomsRoomNameFolderStatusIndexRoute AuthUserChangePasswordUserNameIndexRoute: typeof AuthUserChangePasswordUserNameIndexRoute AuthUserRoleRoleIdIndexRoute: typeof AuthUserRoleRoleIdIndexRoute } @@ -445,6 +486,7 @@ const AuthRouteChildren: AuthRouteChildren = { AuthDeviceIndexRoute: AuthDeviceIndexRoute, AuthRoleIndexRoute: AuthRoleIndexRoute, AuthRoomsIndexRoute: AuthRoomsIndexRoute, + AuthUserIndexRoute: AuthUserIndexRoute, AuthProfileUserNameIndexRoute: AuthProfileUserNameIndexRoute, AuthProfileChangePasswordIndexRoute: AuthProfileChangePasswordIndexRoute, AuthRoleCreateIndexRoute: AuthRoleCreateIndexRoute, @@ -452,6 +494,8 @@ const AuthRouteChildren: AuthRouteChildren = { AuthUserCreateIndexRoute: AuthUserCreateIndexRoute, AuthUserRoleIndexRoute: AuthUserRoleIndexRoute, AuthRoleIdEditIndexRoute: AuthRoleIdEditIndexRoute, + AuthRoomsRoomNameFolderStatusIndexRoute: + AuthRoomsRoomNameFolderStatusIndexRoute, AuthUserChangePasswordUserNameIndexRoute: AuthUserChangePasswordUserNameIndexRoute, AuthUserRoleRoleIdIndexRoute: AuthUserRoleRoleIdIndexRoute, diff --git a/src/routes/_auth/commands/index.tsx b/src/routes/_auth/commands/index.tsx index 58713a8..391f1b6 100644 --- a/src/routes/_auth/commands/index.tsx +++ b/src/routes/_auth/commands/index.tsx @@ -13,7 +13,6 @@ import { import { toast } from "sonner"; 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 +21,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 +45,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 @@ -283,7 +296,7 @@ function CommandPage() { <> 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..6e39fce 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); @@ -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/index.tsx b/src/routes/_auth/user/index.tsx new file mode 100644 index 0000000..e71497f --- /dev/null +++ b/src/routes/_auth/user/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_auth/user/')({ + component: RouteComponent, +}) + +function RouteComponent() { + return
Hello "/_auth/user/"!
+} 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/types/app-sidebar.ts b/src/types/app-sidebar.ts index 027da65..60e9916 100644 --- a/src/types/app-sidebar.ts +++ b/src/types/app-sidebar.ts @@ -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/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