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 ? (
+
+
+ Có
+
+ ) : (
+
+
+ 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}