From eb4243ee5b90671ae16e8dc6c02918f763021633 Mon Sep 17 00:00:00 2001 From: phuongdm Date: Wed, 3 Dec 2025 18:26:36 +0700 Subject: [PATCH] add setup folder scan --- src/components/buttons/delete-button.tsx | 38 ++-- src/components/cards/computer-card.tsx | 17 ++ src/components/folder-status-popover.tsx | 145 +++++++++++++++ src/components/forms/upload-file-form.tsx | 2 +- src/components/grids/device-grid.tsx | 27 ++- src/components/menu/delete-menu.tsx | 140 ++++++++++++++ src/components/tables/device-table.tsx | 31 +++- src/config/api.ts | 13 +- src/hooks/useClientFolderStatus.ts | 83 +++++++++ src/hooks/useMutationData.ts | 10 +- src/routes/_authenticated/agent/index.tsx | 10 +- src/routes/_authenticated/apps/index.tsx | 174 ++++++++++++++---- .../_authenticated/room/$roomName/index.tsx | 92 +++++++-- src/routes/_authenticated/room/index.tsx | 33 +--- src/template/app-manager-template.tsx | 36 +++- src/types/file.ts | 9 + 16 files changed, 747 insertions(+), 113 deletions(-) create mode 100644 src/components/folder-status-popover.tsx create mode 100644 src/components/menu/delete-menu.tsx create mode 100644 src/hooks/useClientFolderStatus.ts create mode 100644 src/types/file.ts diff --git a/src/components/buttons/delete-button.tsx b/src/components/buttons/delete-button.tsx index 402f0cc..7b96b15 100644 --- a/src/components/buttons/delete-button.tsx +++ b/src/components/buttons/delete-button.tsx @@ -23,9 +23,9 @@ export function DeleteButton({ onClick, loading = false, disabled = false, - label = "Xóa", - title = "Xác nhận xóa", - description = "Bạn có chắc chắn muốn xóa các mục này không?", + label = "Xóa khỏi server", + title = "Xóa khỏi server", + description = "Bạn có chắc chắn muốn xóa các phần mềm này khỏi server không? Hành động này không thể hoàn tác.", }: DeleteButtonProps) { const [open, setOpen] = useState(false); const [isConfirming, setIsConfirming] = useState(false); @@ -46,23 +46,28 @@ export function DeleteButton({ variant="destructive" onClick={() => setOpen(true)} disabled={loading || disabled} - className="gap-2" + className="gap-2 px-4" > - - {label} + {loading || isConfirming ? ( + + ) : ( + + )} + {loading || isConfirming ? "Đang xóa..." : label} - + - {title} - {description} + {title} + {description} - + @@ -70,8 +75,19 @@ export function DeleteButton({ variant="destructive" onClick={handleConfirm} disabled={isConfirming || loading} + className="flex-1 gap-2" > - {isConfirming ? "Đang xóa..." : "Xóa"} + {isConfirming ? ( + <> + + Đang xóa... + + ) : ( + <> + + {label} + + )} diff --git a/src/components/cards/computer-card.tsx b/src/components/cards/computer-card.tsx index ac89b39..e1a1d5b 100644 --- a/src/components/cards/computer-card.tsx +++ b/src/components/cards/computer-card.tsx @@ -2,13 +2,19 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover 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"; export function ComputerCard({ device, position, + folderStatus, + isCheckingFolder, }: { device: any | undefined; position: number; + folderStatus?: ClientFolderStatus; + isCheckingFolder?: boolean; }) { if (!device) { return ( @@ -98,6 +104,17 @@ export function ComputerCard({ {position} + {/* Folder Status Icon */} + {device && !isOffline && ( +
+ +
+ )} + {firstNetworkInfo?.ipAddress && (
diff --git a/src/components/folder-status-popover.tsx b/src/components/folder-status-popover.tsx new file mode 100644 index 0000000..6a00caf --- /dev/null +++ b/src/components/folder-status-popover.tsx @@ -0,0 +1,145 @@ +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { CheckCircle2, AlertCircle, Loader2, AlertTriangle } from "lucide-react"; +import type { ClientFolderStatus } from "@/hooks/useClientFolderStatus"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Badge } from "@/components/ui/badge"; + +interface FolderStatusPopoverProps { + deviceId: string; + status?: ClientFolderStatus; + isLoading?: boolean; +} + +export function FolderStatusPopover({ + deviceId, + status, + isLoading, +}: FolderStatusPopoverProps) { + const hasMissing = status && status.missingFiles.length > 0; + const hasExtra = status && status.extraFiles.length > 0; + const hasIssues = hasMissing || hasExtra; + + // Xác định màu sắc và icon dựa trên trạng thái + let statusColor = "text-green-500"; + let statusIcon = ( + + ); + + if (isLoading) { + statusColor = "text-blue-500"; + statusIcon = ; + } else if (hasMissing && hasExtra) { + // Vừa thiếu vừa thừa -> Đỏ + Alert + statusColor = "text-red-600"; + statusIcon = ; + } else if (hasMissing) { + // Chỉ thiếu -> Đỏ + statusColor = "text-red-500"; + statusIcon = ; + } else if (hasExtra) { + // Chỉ thừa -> Cam + statusColor = "text-orange-500"; + statusIcon = ; + } + + return ( + + + + + +
+
+
Thư mục Setup: {deviceId}
+ {hasIssues && ( + + {hasMissing && hasExtra + ? "Không đồng bộ" + : hasMissing + ? "Thiếu file" + : "Thừa file"} + + )} +
+ + {isLoading ? ( +
+ + Đang kiểm tra... +
+ ) : !status ? ( +
+ Chưa có dữ liệu +
+ ) : ( +
+ {/* File thiếu */} + {hasMissing && ( +
+

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

+ +
+ {status.missingFiles.map((file, idx) => ( +
+
+ {file.fileName} +
+
+ {file.folderPath} +
+
+ ))} +
+
+
+ )} + + {/* File thừa */} + {hasExtra && ( +
+

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

+ +
+ {status.extraFiles.map((file, idx) => ( +
+
+ {file.fileName} +
+
+ {file.folderPath} +
+
+ ))} +
+
+
+ )} + + {/* Trạng thái OK */} + {!hasIssues && ( +
+ + Thư mục đạt yêu cầu +
+ )} +
+ )} +
+
+
+ ); +} diff --git a/src/components/forms/upload-file-form.tsx b/src/components/forms/upload-file-form.tsx index 792be73..9a4ff19 100644 --- a/src/components/forms/upload-file-form.tsx +++ b/src/components/forms/upload-file-form.tsx @@ -18,7 +18,7 @@ export function UploadVersionForm({ onSubmit, closeDialog }: UploadVersionFormPr const [isDone, setIsDone] = useState(false); // Match server allowed extensions - const ALLOWED_EXTENSIONS = [".exe", ".apk", ".conf", ".json", ".xml", ".setting", ".lnk", ".url", ".seb"]; + const ALLOWED_EXTENSIONS = [".exe", ".apk", ".conf", ".json", ".xml", ".setting", ".lnk", ".url", ".seb", ".ps1"]; const isFileValid = (file: File) => { const fileName = file.name.toLowerCase(); return ALLOWED_EXTENSIONS.some((ext) => fileName.endsWith(ext)); diff --git a/src/components/grids/device-grid.tsx b/src/components/grids/device-grid.tsx index d7d6a87..04ad38d 100644 --- a/src/components/grids/device-grid.tsx +++ b/src/components/grids/device-grid.tsx @@ -1,8 +1,17 @@ import { Monitor, DoorOpen } from "lucide-react"; import { ComputerCard } from "../cards/computer-card"; import { useMachineNumber } from "../../hooks/useMachineNumber"; +import type { ClientFolderStatus } from "@/hooks/useClientFolderStatus"; -export function DeviceGrid({ devices }: { devices: any[] }) { +export function DeviceGrid({ + devices, + folderStatuses, + isCheckingFolder, +}: { + devices: any[]; + folderStatuses?: Map; + isCheckingFolder?: boolean; +}) { const getMachineNumber = useMachineNumber(); const deviceMap = new Map(); @@ -23,11 +32,17 @@ export function DeviceGrid({ devices }: { devices: any[] }) { {/* Bên trái (21–40) */} {Array.from({ length: 4 }).map((_, i) => { const pos = leftStart + (3 - i); + const device = deviceMap.get(pos); + const macAddress = device?.networkInfos?.[0]?.macAddress || device?.id; + const folderStatus = folderStatuses?.get(macAddress); + return ( ); })} @@ -40,11 +55,17 @@ export function DeviceGrid({ devices }: { devices: any[] }) { {/* Bên phải (1–20) */} {Array.from({ length: 4 }).map((_, i) => { const pos = rightStart + (3 - i); + const device = deviceMap.get(pos); + const macAddress = device?.networkInfos?.[0]?.macAddress || device?.id; + const folderStatus = folderStatuses?.get(macAddress); + return ( ); })} diff --git a/src/components/menu/delete-menu.tsx b/src/components/menu/delete-menu.tsx new file mode 100644 index 0000000..81a1cac --- /dev/null +++ b/src/components/menu/delete-menu.tsx @@ -0,0 +1,140 @@ +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Loader2, Trash2, ChevronDown, AlertTriangle } from "lucide-react"; +import { useState } from "react"; + +interface DeleteMenuProps { + onDeleteFromServer: () => void; + onDeleteFromRequired: () => void; + loading?: boolean; + label?: string; + serverLabel?: string; + requiredLabel?: string; +} + +export function DeleteMenu({ + onDeleteFromServer, + onDeleteFromRequired, + loading, + label = "Xóa", + serverLabel = "Xóa khỏi server", + requiredLabel = "Xóa khỏi danh sách yêu cầu", +}: DeleteMenuProps) { + const [open, setOpen] = useState(false); + const [showConfirmDelete, setShowConfirmDelete] = useState(false); + + const handleDeleteFromServer = async () => { + try { + await onDeleteFromServer(); + } finally { + setOpen(false); + setShowConfirmDelete(false); + } + }; + + const handleDeleteFromRequired = async () => { + try { + await onDeleteFromRequired(); + } finally { + setOpen(false); + } + }; + + return ( + <> + + + + + + + + {requiredLabel} + + + setShowConfirmDelete(true)} + disabled={loading} + className="focus:bg-red-50 focus:text-red-900" + > + + {serverLabel} + + + + + {/* Confirmation Dialog for Delete from Server */} + {showConfirmDelete && ( +
+
+
+ +

Cảnh báo: Xóa khỏi server

+
+

+ Bạn đang chuẩn bị xóa các phần mềm này khỏi server. Hành động này không thể hoàn tác và sẽ xóa vĩnh viễn tất cả các tệp liên quan. +

+

+ Vui lòng chắc chắn trước khi tiếp tục. +

+
+ + +
+
+
+ )} + + ); +} diff --git a/src/components/tables/device-table.tsx b/src/components/tables/device-table.tsx index bf35420..fbc145b 100644 --- a/src/components/tables/device-table.tsx +++ b/src/components/tables/device-table.tsx @@ -18,15 +18,23 @@ import { Badge } from "@/components/ui/badge"; 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; } /** * Component hiển thị danh sách thiết bị ở dạng bảng */ -export function DeviceTable({ devices }: DeviceTableProps) { +export function DeviceTable({ + devices, + folderStatuses, + isCheckingFolder, +}: DeviceTableProps) { const getMachineNumber = useMachineNumber(); const columns: ColumnDef[] = [ @@ -137,6 +145,27 @@ export function DeviceTable({ devices }: DeviceTableProps) { ); }, }, + { + header: "Thư mục Setup", + cell: ({ row }) => { + const device = row.original; + const isOffline = device.isOffline; + const macAddress = device.networkInfos?.[0]?.macAddress || device.id; + const folderStatus = folderStatuses?.get(macAddress); + + if (isOffline) { + return -; + } + + return ( + + ); + }, + }, ]; const table = useReactTable({ diff --git a/src/config/api.ts b/src/config/api.ts index 1309725..3cb4c24 100644 --- a/src/config/api.ts +++ b/src/config/api.ts @@ -6,15 +6,23 @@ export const BASE_URL = isDev export const API_ENDPOINTS = { APP_VERSION: { + //agent and app api GET_VERSION: `${BASE_URL}/AppVersion/version`, UPLOAD: `${BASE_URL}/AppVersion/upload`, GET_SOFTWARE: `${BASE_URL}/AppVersion/msifiles`, + + //blacklist api GET_BLACKLIST: `${BASE_URL}/AppVersion/blacklist`, ADD_BLACKLIST: `${BASE_URL}/AppVersion/blacklist/add`, DELETE_BLACKLIST: (appId: number) => `${BASE_URL}/AppVersion/blacklist/remove/${appId}`, UPDATE_BLACKLIST: (appId: string) => `${BASE_URL}/AppVersion/blacklist/update/${appId}`, - DELETE_FILES: `${BASE_URL}/AppVersion/delete`, REQUEST_UPDATE_BLACKLIST: `${BASE_URL}/AppVersion/blacklist/request-update`, + + //require file api + GET_REQUIRED_FILES: `${BASE_URL}/AppVersion/requirefiles`, + ADD_REQUIRED_FILE: `${BASE_URL}/AppVersion/requirefile/add`, + DELETE_REQUIRED_FILE: (fileId: number) => `${BASE_URL}/AppVersion/requirefile/delete/${fileId}`, + DELETE_FILES: (fileId: number) => `${BASE_URL}/AppVersion/delete/${fileId}`, }, DEVICE_COMM: { DOWNLOAD_FILES: (roomName: string) => `${BASE_URL}/DeviceComm/downloadfile/${roomName}`, @@ -27,10 +35,13 @@ export const API_ENDPOINTS = { UPDATE_BLACKLIST: (roomName: string) => `${BASE_URL}/DeviceComm/updateblacklist/${roomName}`, 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}`, }, SSE_EVENTS: { DEVICE_ONLINE: `${BASE_URL}/Sse/events/onlineDevices`, DEVICE_OFFLINE: `${BASE_URL}/Sse/events/offlineDevices`, GET_PROCESSES_LISTS: `${BASE_URL}/Sse/events/processLists`, + GET_CLIENT_FOLDER_STATUS: `${BASE_URL}/Sse/events/clientFolderStatuses`, }, }; diff --git a/src/hooks/useClientFolderStatus.ts b/src/hooks/useClientFolderStatus.ts new file mode 100644 index 0000000..3ec0bec --- /dev/null +++ b/src/hooks/useClientFolderStatus.ts @@ -0,0 +1,83 @@ +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/hooks/useMutationData.ts b/src/hooks/useMutationData.ts index 7cfaccc..27dfb1b 100644 --- a/src/hooks/useMutationData.ts +++ b/src/hooks/useMutationData.ts @@ -42,10 +42,14 @@ export function useMutationData({ }); return response.data; }, - onSuccess: (data) => { - invalidate.forEach((key) => - queryClient.invalidateQueries({ queryKey: key }) + onSuccess: async (data) => { + // Invalidate queries trước + await Promise.all( + invalidate.map((key) => + queryClient.invalidateQueries({ queryKey: key }) + ) ); + // Sau đó gọi callback onSuccess?.(data); }, onError, diff --git a/src/routes/_authenticated/agent/index.tsx b/src/routes/_authenticated/agent/index.tsx index 2d2b6ad..a062101 100644 --- a/src/routes/_authenticated/agent/index.tsx +++ b/src/routes/_authenticated/agent/index.tsx @@ -6,15 +6,7 @@ import { BASE_URL, API_ENDPOINTS } from "@/config/api"; import { toast } from "sonner"; import type { ColumnDef } from "@tanstack/react-table"; import type { AxiosProgressEvent } from "axios"; - -type Version = { - id?: string; - version: string; - fileName: string; - folderPath: string; - updatedAt?: string; - requestUpdateAt?: string; -}; +import type { Version } from "@/types/file"; export const Route = createFileRoute("/_authenticated/agent/")({ head: () => ({ meta: [{ title: "Quản lý Agent" }] }), diff --git a/src/routes/_authenticated/apps/index.tsx b/src/routes/_authenticated/apps/index.tsx index f62c630..152f176 100644 --- a/src/routes/_authenticated/apps/index.tsx +++ b/src/routes/_authenticated/apps/index.tsx @@ -7,21 +7,14 @@ import { toast } from "sonner"; import type { ColumnDef } from "@tanstack/react-table"; import { useState } from "react"; import type { AxiosProgressEvent } from "axios"; +import type { Version } from "@/types/file"; +import { Check, X } from "lucide-react"; export const Route = createFileRoute("/_authenticated/apps/")({ head: () => ({ meta: [{ title: "Quản lý phần mềm" }] }), component: AppsComponent, }); -type Version = { - id: number; - version: string; - fileName: string; - folderPath: string; - updatedAt?: string; - requestUpdateAt?: string; -}; - function AppsComponent() { const { data, isLoading } = useQueryData({ queryKey: ["software-version"], @@ -74,7 +67,7 @@ function AppsComponent() { const deleteMutation = useMutationData<{ MsiFileIds: number[] }>({ url: BASE_URL + API_ENDPOINTS.APP_VERSION.DELETE_FILES, - method: "DELETE", + method: "POST", invalidate: [["software-version"]], onSuccess: () => toast.success("Xóa phần mềm thành công!"), onError: (error) => { @@ -83,6 +76,31 @@ function AppsComponent() { }, }); + const addRequiredFileMutation = useMutationData<{ + fileName: string; + version: string; + }>({ + url: BASE_URL + API_ENDPOINTS.APP_VERSION.ADD_REQUIRED_FILE, + method: "POST", + invalidate: [["software-version"]], + onSuccess: () => toast.success("Thêm file vào danh sách thành công!"), + onError: (error) => { + console.error("Add required file error:", error); + toast.error("Thêm file vào danh sách thất bại!"); + }, + }); + + const deleteRequiredFileMutation = useMutationData<{ id: number }>({ + url: "", + method: "POST", + invalidate: [["software-version"]], + onSuccess: () => toast.success("Xóa file khỏi danh sách thành công!"), + onError: (error) => { + console.error("Delete required file error:", error); + toast.error("Xóa file khỏi danh sách thất bại!"); + }, + }); + // Cột bảng const columns: ColumnDef[] = [ { accessorKey: "version", header: "Phiên bản" }, @@ -90,7 +108,7 @@ function AppsComponent() { { accessorKey: "folderPath", header: "Đường dẫn" }, { accessorKey: "updatedAt", - header: "Thời gian cập nhật", + header: () =>
Thời gian cập nhật
, cell: ({ getValue }) => getValue() ? new Date(getValue() as string).toLocaleString("vi-VN") @@ -98,11 +116,35 @@ function AppsComponent() { }, { accessorKey: "requestUpdateAt", - header: "Thời gian yêu cầu cài đặt", + header: () =>
Thời gian yêu cầu cài đặt/tải xuống
, + cell: ({ getValue }) => + getValue() + ? new Date(getValue() as string).toLocaleString("vi-VN") + : "N/A", + }, + { + id: "required", + header: () =>
Đã thêm vào danh sách
, + cell: ({ row }) => { + const isRequired = row.original.isRequired; + return isRequired ? ( +
+ + +
+ ) : ( +
+ + Không +
+ ); + }, + enableSorting: false, + enableHiding: false, }, { id: "select", - header: () => Thêm vào danh sách yêu cầu, + header: () =>
Chọn
, cell: ({ row }) => ( row.original.id); + const handleDeleteFromRequiredList = async () => { + if (!table) return; + + const selectedRows = table.getSelectedRowModel().rows; try { - await deleteMutation.mutateAsync({ - data: { MsiFileIds }, - }); + for (const row of selectedRows) { + const { id } = row.original; + await deleteRequiredFileMutation.mutateAsync({ + data: { id }, + url: BASE_URL + API_ENDPOINTS.APP_VERSION.DELETE_REQUIRED_FILE(id), + }); + } + if (table) { + table.setRowSelection({}); + } + } catch (e) { + console.error("Delete from required list error:", e); + toast.error("Có lỗi xảy ra khi xóa!"); + } + }; + + const handleDeleteFromServer = async () => { + if (!table) return; + + const selectedRows = table.getSelectedRowModel().rows; + + try { + for (const row of selectedRows) { + const { id } = row.original; + await deleteMutation.mutateAsync({ + data: { MsiFileIds: [id] }, + url: BASE_URL + API_ENDPOINTS.APP_VERSION.DELETE_FILES(id), + }); + } + if (table) { + table.setRowSelection({}); + } } catch (e) { console.error("Delete error:", e); toast.error("Có lỗi xảy ra khi xóa!"); } }; + const handleAddToRequired = async () => { + if (!table) { + toast.error("Không thể lấy thông tin bảng!"); + return; + } + + const selectedRows = table.getSelectedRowModel().rows; + if (selectedRows.length === 0) { + toast.error("Vui lòng chọn ít nhất một file!"); + return; + } + + try { + for (const row of selectedRows) { + const { fileName, version } = row.original; + await addRequiredFileMutation.mutateAsync({ + data: { fileName, version }, + }); + } + table.setRowSelection({}); + } catch (e) { + console.error("Add required file error:", e); + toast.error("Có lỗi xảy ra!"); + } + }; + return ( - - title="Quản lý phần mềm" - description="Quản lý và gửi yêu cầu cài đặt phần mềm hoặc file cấu hình" - data={versionList} - isLoading={isLoading} - columns={columns} - onUpload={handleUpload} - onUpdate={handleInstall} - onDownload={handleDonwload} - onDelete={handleDelete} - updateLoading={installMutation.isPending} - downloadLoading={downloadMutation.isPending} - deleteLoading={deleteMutation.isPending} - onTableInit={setTable} - rooms={roomData} - /> + <> + + title="Quản lý phần mềm" + uploadFormTitle="Tải lên || Cập nhật file phần mềm" + description="Quản lý và gửi yêu cầu cài đặt phần mềm hoặc file cấu hình" + data={versionList} + isLoading={isLoading} + columns={columns} + onUpload={handleUpload} + onUpdate={handleInstall} + onDownload={handleDonwload} + onDelete={handleDelete} + onDeleteFromServer={handleDeleteFromServer} + onDeleteFromRequired={handleDeleteFromRequiredList} + onAddToRequired={handleAddToRequired} + updateLoading={installMutation.isPending} + downloadLoading={downloadMutation.isPending} + deleteLoading={deleteMutation.isPending || deleteRequiredFileMutation.isPending} + addToRequiredLoading={addRequiredFileMutation.isPending} + onTableInit={setTable} + rooms={roomData} + /> + ); } diff --git a/src/routes/_authenticated/room/$roomName/index.tsx b/src/routes/_authenticated/room/$roomName/index.tsx index e14b424..a09eef2 100644 --- a/src/routes/_authenticated/room/$roomName/index.tsx +++ b/src/routes/_authenticated/room/$roomName/index.tsx @@ -1,13 +1,16 @@ import { createFileRoute, useParams } from "@tanstack/react-router"; import { useState } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { LayoutGrid, TableIcon, Monitor } from "lucide-react"; +import { LayoutGrid, TableIcon, Monitor, FolderCheck, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useQueryData } from "@/hooks/useQueryData"; +import { useDeviceEvents } from "@/hooks/useDeviceEvents"; +import { useClientFolderStatus } from "@/hooks/useClientFolderStatus"; import { API_ENDPOINTS, BASE_URL } from "@/config/api"; import { DeviceGrid } from "@/components/grids/device-grid"; import { DeviceTable } from "@/components/tables/device-table"; import { useMachineNumber } from "@/hooks/useMachineNumber"; +import { toast } from "sonner"; export const Route = createFileRoute("/_authenticated/room/$roomName/")({ head: ({ params }) => ({ @@ -19,6 +22,14 @@ export const Route = createFileRoute("/_authenticated/room/$roomName/")({ function RoomDetailPage() { const { roomName } = useParams({ from: "/_authenticated/room/$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); + const { data: devices = [] } = useQueryData({ queryKey: ["devices", roomName], url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.GET_DEVICE_FROM_ROOM(roomName), @@ -26,6 +37,28 @@ function RoomDetailPage() { const parseMachineNumber = useMachineNumber(); + const handleCheckFolderStatus = async () => { + try { + setIsCheckingFolder(true); + const response = await fetch( + BASE_URL + API_ENDPOINTS.DEVICE_COMM.REQUEST_GET_CLIENT_FOLDER_STATUS(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!"); + setIsCheckingFolder(false); + } + }; + const sortedDevices = [...devices].sort((a, b) => { return parseMachineNumber(a.id) - parseMachineNumber(b.id); }); @@ -39,25 +72,42 @@ function RoomDetailPage() { Danh sách thiết bị phòng {roomName} -
+
- + +
+ + +
@@ -71,9 +121,17 @@ function RoomDetailPage() {

) : viewMode === "grid" ? ( - + ) : ( - + )} diff --git a/src/routes/_authenticated/room/index.tsx b/src/routes/_authenticated/room/index.tsx index 07173a1..4feafb8 100644 --- a/src/routes/_authenticated/room/index.tsx +++ b/src/routes/_authenticated/room/index.tsx @@ -26,7 +26,6 @@ import { ChevronRight, Loader2, Wifi, - WifiOff, } from "lucide-react"; import React from "react"; @@ -77,34 +76,16 @@ function RoomComponent() { ), }, { - header: "Số lượng thiết bị", - accessorKey: "numberOfDevices", - cell: ({ row }) => ( -
- - - {row.original.numberOfDevices} thiết bị - -
- ), - }, - { - header: "Thiết bị offline", - accessorKey: "numberOfOfflineDevices", + header: "Số lượng thiết bị online", cell: ({ row }) => { - const offlineCount = row.original.numberOfOfflineDevices; - const isOffline = offlineCount > 0; - + const onlineCount = row.original.numberOfDevices - row.original.numberOfOfflineDevices; + const totalCount = row.original.numberOfDevices; + return (
- - - {offlineCount} offline + + + {onlineCount} / {totalCount}
); diff --git a/src/template/app-manager-template.tsx b/src/template/app-manager-template.tsx index c37e78d..a3c6a69 100644 --- a/src/template/app-manager-template.tsx +++ b/src/template/app-manager-template.tsx @@ -11,7 +11,8 @@ import { FileText, Building2, Download } from "lucide-react"; import { FormDialog } from "@/components/dialogs/form-dialog"; import { VersionTable } from "@/components/tables/version-table"; import { RequestUpdateMenu } from "@/components/menu/request-update-menu"; -import { DeleteButton } from "@/components/buttons/delete-button"; +import { DeleteMenu } from "@/components/menu/delete-menu"; +import { Button } from "@/components/ui/button"; import type { AxiosProgressEvent } from "axios"; import { useState } from "react"; import { SelectDialog } from "@/components/dialogs/select-dialog"; @@ -23,6 +24,7 @@ import { fetchDevicesFromRoom } from "@/services/device.service"; interface AppManagerTemplateProps { title: string; + uploadFormTitle?: string; description: string; data: TData[]; isLoading: boolean; @@ -36,7 +38,11 @@ interface AppManagerTemplateProps { onDownload?: (targetNames: string[]) => Promise | void; downloadLoading?: boolean; onDelete?: () => Promise | void; + onDeleteFromServer?: () => Promise | void; + onDeleteFromRequired?: () => Promise | void; deleteLoading?: boolean; + onAddToRequired?: () => Promise | void; + addToRequiredLoading?: boolean; onTableInit?: (table: any) => void; rooms?: Room[]; devices?: string[]; @@ -44,6 +50,7 @@ interface AppManagerTemplateProps { export function AppManagerTemplate({ title, + uploadFormTitle, description, data, isLoading, @@ -54,7 +61,11 @@ export function AppManagerTemplate({ onDownload, downloadLoading, onDelete, + onDeleteFromServer, + onDeleteFromRequired, deleteLoading, + onAddToRequired, + addToRequiredLoading, onTableInit, rooms = [], devices = [], @@ -112,8 +123,8 @@ export function AppManagerTemplate({

{description}

{(closeDialog) => ( @@ -138,7 +149,7 @@ export function AppManagerTemplate({ /> - {(onUpdate || onDelete) && ( + {(onUpdate || onDelete || onAddToRequired) && (
({ icon={} /> )} + {onAddToRequired && ( + + )}
- {onDelete && ( - )}
diff --git a/src/types/file.ts b/src/types/file.ts new file mode 100644 index 0000000..ff12fe3 --- /dev/null +++ b/src/types/file.ts @@ -0,0 +1,9 @@ +export type Version = { + id: number; + version: string; + fileName: string; + folderPath: string; + updatedAt?: string; + requestUpdateAt?: string; + isRequired: boolean; +};