diff --git a/package-lock.json b/package-lock.json index e8db8ff..60b65a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-progress": "^1.1.7", @@ -1579,6 +1580,41 @@ } } }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", @@ -1660,6 +1696,150 @@ } } }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popover": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", diff --git a/package.json b/package.json index 2fc1438..9d68cd9 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-progress": "^1.1.7", diff --git a/src/components/add-new-dialog.tsx b/src/components/add-new-dialog.tsx new file mode 100644 index 0000000..c83e554 --- /dev/null +++ b/src/components/add-new-dialog.tsx @@ -0,0 +1,140 @@ +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { toast } from "sonner"; +import { useForm, formOptions } from "@tanstack/react-form"; +import axios from "axios"; + +interface AddBlacklistDialogProps { + onAdded?: () => void; // callback để refresh danh sách sau khi thêm +} + +const formOpts = formOptions({ + defaultValues: { appName: "", processName: "" }, +}); + +export function AddBlacklistDialog({ onAdded }: AddBlacklistDialogProps) { + const [isOpen, setIsOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isDone, setIsDone] = useState(false); + + const form = useForm({ + ...formOpts, + onSubmit: async ({ value }) => { + if (!value.appName || !value.processName) { + toast.error("Vui lòng nhập đầy đủ tên ứng dụng và tiến trình"); + return; + } + + try { + setIsSubmitting(true); + + await axios.post("/api/appversions/add-blacklist", { + appName: value.appName, + processName: value.processName, + }); + + toast.success("Đã thêm vào blacklist!"); + setIsDone(true); + + if (onAdded) onAdded(); + } catch (error) { + console.error(error); + toast.error("Không thể thêm vào blacklist"); + } finally { + setIsSubmitting(false); + } + }, + }); + + const handleDialogClose = (open: boolean) => { + if (isSubmitting) return; + setIsOpen(open); + if (!open) { + setIsDone(false); + form.reset(); + } + }; + + return ( + + + + + + + + Thêm ứng dụng vào danh sách cấm + + +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + > + + {(field) => ( +
+ + field.handleChange(e.target.value)} + placeholder="Ví dụ: Google Chrome" + disabled={isSubmitting || isDone} + /> +
+ )} +
+ + + {(field) => ( +
+ + field.handleChange(e.target.value)} + placeholder="chrome.exe" + disabled={isSubmitting || isDone} + /> +
+ )} +
+ + + {!isDone ? ( + <> + + + + ) : ( + + )} + +
+
+
+ ); +} diff --git a/src/components/device-grid.tsx b/src/components/device-grid.tsx index 254143c..75b3bdc 100644 --- a/src/components/device-grid.tsx +++ b/src/components/device-grid.tsx @@ -11,23 +11,35 @@ export function DeviceGrid({ devices }: { devices: any[] }) { if (number > 0 && number <= 40) deviceMap.set(number, device); }); - const computersPerRow = 8; const totalRows = 5; const renderRow = (rowIndex: number) => { - const start = rowIndex * computersPerRow + 1; + // Trái: 1–20 + const leftStart = rowIndex * 4 + 1; + // Phải: 21–40 + const rightStart = 21 + rowIndex * 4; + return (
+ {/* Bên trái (1–20) */} {Array.from({ length: 4 }).map((_, i) => { - const pos = start + i; - return ; + const pos = leftStart + i; + return ( + + ); })} + + {/* Đường chia giữa */}
+ + {/* Bên phải (21–40) */} {Array.from({ length: 4 }).map((_, i) => { - const pos = start + i + 4; - return ; + const pos = rightStart + i; + return ( + + ); })}
); diff --git a/src/components/request-update-menu.tsx b/src/components/request-update-menu.tsx new file mode 100644 index 0000000..ef51d71 --- /dev/null +++ b/src/components/request-update-menu.tsx @@ -0,0 +1,65 @@ +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + DropdownMenuSeparator, +} from "@/components/ui/dropdown-menu"; +import { Loader2, RefreshCw, ChevronDown } from "lucide-react"; + +interface RequestUpdateMenuProps { + onUpdateDevice: () => void; + onUpdateRoom: () => void; + onUpdateAll: () => void; + loading?: boolean; +} + +export function RequestUpdateMenu({ + onUpdateDevice, + onUpdateRoom, + onUpdateAll, + loading, +}: RequestUpdateMenuProps) { + return ( + + + + + + + + Cập nhật thiết bị cụ thể + + + + + Cập nhật theo phòng + + + + + Cập nhật tất cả thiết bị + + + + ); +} diff --git a/src/components/room-select-dialog.tsx b/src/components/room-select-dialog.tsx deleted file mode 100644 index 937addf..0000000 --- a/src/components/room-select-dialog.tsx +++ /dev/null @@ -1,98 +0,0 @@ -"use client" - -import { useState } from "react" -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, -} from "@/components/ui/dialog" -import { Button } from "@/components/ui/button" -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" -import { Label } from "@/components/ui/label" -import { Check, Home } from "lucide-react" - -interface RoomSelectDialogProps { - open: boolean - onClose: () => void - rooms: string[] - onConfirm: (roomName: string) => void -} - -export function RoomSelectDialog({ - open, - onClose, - rooms, - onConfirm, -}: RoomSelectDialogProps) { - const [selectedRoom, setSelectedRoom] = useState("") - - return ( - - - -
- -
- - Chọn phòng để cập nhật - -

- Vui lòng chọn phòng để gửi lệnh cập nhật -

-
- -
- - {rooms.map((room) => ( -
-
- - -
- - {selectedRoom === room && ( -
- -
- )} -
- ))} -
-
- - - - - -
-
- ) -} diff --git a/src/components/select-dialog.tsx b/src/components/select-dialog.tsx new file mode 100644 index 0000000..b8fe53f --- /dev/null +++ b/src/components/select-dialog.tsx @@ -0,0 +1,135 @@ +import { useEffect, useState, useMemo } from "react" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { Label } from "@/components/ui/label" +import { Check, Search } from "lucide-react" +import { Input } from "@/components/ui/input" + +interface SelectDialogProps { + open: boolean + onClose: () => void + items: string[] // danh sách chung: có thể là devices hoặc rooms + title?: string // tiêu đề động + description?: string // mô tả ngắn + icon?: React.ReactNode // icon thay đổi tùy loại + onConfirm: (selected: string[]) => void +} + +export function SelectDialog({ + open, + onClose, + items, + title = "Chọn mục", + description = "Bạn có thể chọn nhiều mục để thao tác", + icon, + onConfirm, +}: SelectDialogProps) { + const [selectedItems, setSelectedItems] = useState([]) + const [search, setSearch] = useState("") + + useEffect(() => { + if (!open) { + setSelectedItems([]) + setSearch("") + } + }, [open]) + + const toggleItem = (item: string) => { + setSelectedItems((prev) => + prev.includes(item) + ? prev.filter((i) => i !== item) + : [...prev, item] + ) + } + + // Lọc danh sách theo từ khóa + const filteredItems = useMemo(() => { + return items.filter((item) => + item.toLowerCase().includes(search.toLowerCase()) + ) + }, [items, search]) + + return ( + + + +
+ {icon ?? } +
+ {title} +

{description}

+
+ + {/* 🔍 Thanh tìm kiếm */} +
+ + setSearch(e.target.value)} + className="pl-9" + /> +
+ + {/* Danh sách các item */} +
+ {filteredItems.length > 0 ? ( + filteredItems.map((item) => ( +
toggleItem(item)} + > +
+ toggleItem(item)} + /> + +
+ + {selectedItems.includes(item) && ( +
+ +
+ )} +
+ )) + ) : ( +

+ Không tìm thấy kết quả +

+ )} +
+ + + + + +
+
+ ) +} diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..eaed9ba --- /dev/null +++ b/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,255 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} diff --git a/src/config/api.ts b/src/config/api.ts index de4e355..c076a3e 100644 --- a/src/config/api.ts +++ b/src/config/api.ts @@ -29,6 +29,5 @@ export const API_ENDPOINTS = { DEVICE_ONLINE: `${BASE_URL}/Sse/events/onlineDevices`, DEVICE_OFFLINE: `${BASE_URL}/Sse/events/offlineDevices`, GET_PROCESSES_LISTS: `${BASE_URL}/Sse/events/processLists`, - }, }; diff --git a/src/routes/_authenticated/agent/index.tsx b/src/routes/_authenticated/agent/index.tsx index acaf9ae..5e52b7d 100644 --- a/src/routes/_authenticated/agent/index.tsx +++ b/src/routes/_authenticated/agent/index.tsx @@ -59,7 +59,7 @@ function AgentsPage() { }); const updateMutation = useMutationData({ - url: "", + url: "", method: "POST", onSuccess: () => toast.success("Đã gửi yêu cầu update!"), onError: () => toast.error("Gửi yêu cầu thất bại!"), @@ -75,13 +75,19 @@ function AgentsPage() { }); }; - // Callback khi chọn phòng update - const handleUpdate = async (roomName: string) => { - return updateMutation.mutateAsync({ - url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.UPDATE_AGENT(roomName), - method: "POST", - data: undefined, - }); + const handleUpdate = async (roomNames: string[]) => { + for (const roomName of roomNames) { + try { + await updateMutation.mutateAsync({ + url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.UPDATE_AGENT(roomName), + method: "POST", + data: undefined + }); + } catch { + toast.error(`Gửi yêu cầu thất bại cho ${roomName}`); + } + } + toast.success("Đã gửi yêu cầu update cho các phòng đã chọn!"); }; // Cột bảng diff --git a/src/routes/_authenticated/apps/index.tsx b/src/routes/_authenticated/apps/index.tsx index 9533523..f6053bb 100644 --- a/src/routes/_authenticated/apps/index.tsx +++ b/src/routes/_authenticated/apps/index.tsx @@ -113,7 +113,7 @@ function AppsComponent() { }; // Callback khi chọn phòng - const handleInstall = async (roomName: string) => { + const handleInstall = async (roomNames: string[]) => { if (!table) { toast.error("Không thể lấy thông tin bảng!"); return; @@ -127,10 +127,14 @@ function AppsComponent() { const MsiFileIds = selectedRows.map((row: any) => row.original.id); - return installMutation.mutateAsync({ - url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.DOWNLOAD_MSI(roomName), // set url động - data: { MsiFileIds }, - }); + for (const roomName of roomNames) { + await installMutation.mutateAsync({ + url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.DOWNLOAD_MSI(roomName), + data: { MsiFileIds }, + }); + } + + toast.success("Đã gửi yêu cầu cài đặt phần mềm cho các phòng đã chọn!"); }; return ( diff --git a/src/routes/_authenticated/room/$roomName/index.tsx b/src/routes/_authenticated/room/$roomName/index.tsx index 00b2369..2cb9c7b 100644 --- a/src/routes/_authenticated/room/$roomName/index.tsx +++ b/src/routes/_authenticated/room/$roomName/index.tsx @@ -17,7 +17,7 @@ export const Route = createFileRoute("/_authenticated/room/$roomName/")({ function RoomDetailPage() { const { roomName } = useParams({ from: "/_authenticated/room/$roomName/" }); - const [viewMode, setViewMode] = useState<"table" | "grid">("table"); + const [viewMode, setViewMode] = useState<"grid" | "table">("grid"); const { data: devices = [] } = useQueryData({ queryKey: ["devices", roomName], url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.GET_DEVICE_FROM_ROOM(roomName), @@ -33,15 +33,6 @@ function RoomDetailPage() {
- +
diff --git a/src/template/app-manager-template.tsx b/src/template/app-manager-template.tsx index 907ef08..18a1205 100644 --- a/src/template/app-manager-template.tsx +++ b/src/template/app-manager-template.tsx @@ -7,13 +7,13 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { FileText } from "lucide-react"; +import { FileText, Building2, Monitor } from "lucide-react"; import { UploadDialog } from "@/components/upload-dialog"; import { VersionTable } from "@/components/version-table"; -import { UpdateButton } from "@/components/update-button"; -import { RoomSelectDialog } from "@/components/room-select-dialog"; +import { RequestUpdateMenu } from "@/components/request-update-menu"; import type { AxiosProgressEvent } from "axios"; import { useState } from "react"; +import { SelectDialog } from "@/components/select-dialog"; // <-- dùng dialog chung interface AppManagerTemplateProps { title: string; @@ -25,10 +25,11 @@ interface AppManagerTemplateProps { fd: FormData, config?: { onUploadProgress?: (e: AxiosProgressEvent) => void } ) => Promise; - onUpdate?: (roomName: string) => void; + onUpdate?: (targetNames: string[]) => Promise | void; updateLoading?: boolean; onTableInit?: (table: any) => void; - rooms: string[]; + rooms?: string[]; + devices?: string[]; } export function AppManagerTemplate({ @@ -41,16 +42,57 @@ export function AppManagerTemplate({ onUpdate, updateLoading, onTableInit, - rooms, + rooms = [], + devices = [], }: AppManagerTemplateProps) { const [dialogOpen, setDialogOpen] = useState(false); - const handleUpdateClick = () => { - if (rooms && onUpdate) { + const [dialogType, setDialogType] = useState<"room" | "device" | null>(null); + + const openRoomDialog = () => { + if (rooms.length > 0 && onUpdate) { + setDialogType("room"); setDialogOpen(true); } }; + + const openDeviceDialog = () => { + if (devices.length > 0 && onUpdate) { + setDialogType("device"); + setDialogOpen(true); + } + }; + + const handleUpdateAll = async () => { + if (!onUpdate) return; + const allTargets = [...rooms, ...devices]; + await onUpdate(allTargets); + }; + + const getDialogProps = () => { + if (dialogType === "room") { + return { + title: "Chọn phòng", + description: "Chọn các phòng cần cập nhật", + icon: , + items: rooms, + }; + } + if (dialogType === "device") { + return { + title: "Chọn thiết bị", + description: "Chọn các thiết bị cần cập nhật", + icon: , + items: devices, + }; + } + return null; + }; + + const dialogProps = getDialogProps(); + return (
+ {/* Header */}

{title}

@@ -59,6 +101,7 @@ export function AppManagerTemplate({
+ {/* Table */} @@ -66,6 +109,7 @@ export function AppManagerTemplate({ Tất cả các phiên bản đã tải lên + ({ onTableInit={onTableInit} /> + {onUpdate && ( - - onUpdate("All")} + )} - {rooms && onUpdate && ( - setDialogOpen(false)} - rooms={rooms} - onConfirm={(roomName) => { - onUpdate(roomName); - setDialogOpen(false); - }} - /> - )} + + {/* 🧩 SelectDialog tái sử dụng */} + {dialogProps && ( + setDialogOpen(false)} + title={dialogProps.title} + description={dialogProps.description} + icon={dialogProps.icon} + items={dialogProps.items} + onConfirm={async (selectedItems) => { + if (!onUpdate) return; + await onUpdate(selectedItems); + setDialogOpen(false); + }} + /> + )}
); } diff --git a/src/template/form-submit-template.tsx b/src/template/form-submit-template.tsx index e0c7817..885eecb 100644 --- a/src/template/form-submit-template.tsx +++ b/src/template/form-submit-template.tsx @@ -1,5 +1,3 @@ -"use client" - import { useState } from "react" import { Card, @@ -9,9 +7,9 @@ import { CardHeader, CardTitle, } from "@/components/ui/card" -import { UpdateButton } from "@/components/update-button" -import { Terminal } from "lucide-react" -import { RoomSelectDialog } from "@/components/room-select-dialog" +import { Terminal, Building2, Monitor } from "lucide-react" +import { RequestUpdateMenu } from "@/components/request-update-menu" +import { SelectDialog } from "@/components/select-dialog" interface FormSubmitTemplateProps { title: string @@ -21,9 +19,10 @@ interface FormSubmitTemplateProps { command: string setCommand: (val: string) => void }) => React.ReactNode - onSubmit?: (roomName: string, command: string) => void + onSubmit?: (target: string, command: string) => void | Promise submitLoading?: boolean rooms?: string[] + devices?: string[] } export function FormSubmitTemplate({ @@ -34,16 +33,56 @@ export function FormSubmitTemplate({ onSubmit, submitLoading, rooms = [], + devices = [], }: FormSubmitTemplateProps) { - const [dialogOpen, setDialogOpen] = useState(false) const [command, setCommand] = useState("") + const [dialogOpen, setDialogOpen] = useState(false) + const [dialogType, setDialogType] = useState<"room" | "device" | null>(null) - const handleClick = () => { + const openRoomDialog = () => { if (rooms.length > 0 && onSubmit) { + setDialogType("room") setDialogOpen(true) } } + const openDeviceDialog = () => { + if (devices.length > 0 && onSubmit) { + setDialogType("device") + setDialogOpen(true) + } + } + + const handleSubmitAll = () => { + if (!onSubmit) return + const allTargets = [...rooms, ...devices] + for (const target of allTargets) { + onSubmit(target, command) + } + } + + const getDialogProps = () => { + if (dialogType === "room") { + return { + title: "Chọn phòng để gửi lệnh", + description: "Chọn các phòng muốn gửi lệnh CMD tới", + icon: , + items: rooms, + } + } + if (dialogType === "device") { + return { + title: "Chọn thiết bị để gửi lệnh", + description: "Chọn các thiết bị muốn gửi lệnh CMD tới", + icon: , + items: devices, + } + } + return null + } + + const dialogProps = getDialogProps() + return (
@@ -58,33 +97,35 @@ export function FormSubmitTemplate({ Nhập và gửi lệnh xuống thiết bị - - {children({ command, setCommand })} - + + {children({ command, setCommand })} {onSubmit && ( - - + - onSubmit("All", command)} - loading={submitLoading} - label="Cập nhật tất cả thiết bị" /> )} - {onSubmit && rooms.length > 0 && ( - setDialogOpen(false)} - rooms={rooms} - onConfirm={(roomName) => { - onSubmit(roomName, command) + title={dialogProps.title} + description={dialogProps.description} + icon={dialogProps.icon} + items={dialogProps.items} + onConfirm={async (selectedItems) => { + if (!onSubmit) return + for (const item of selectedItems) { + await onSubmit(item, command) + } setDialogOpen(false) }} /> diff --git a/src/template/table-manager-template.tsx b/src/template/table-manager-template.tsx new file mode 100644 index 0000000..f758338 --- /dev/null +++ b/src/template/table-manager-template.tsx @@ -0,0 +1,153 @@ +import { RequestUpdateMenu } from "@/components/request-update-menu"; +import { SelectDialog } from "@/components/select-dialog"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { UploadDialog } from "@/components/upload-dialog"; +import { VersionTable } from "@/components/version-table"; +import type { ColumnDef } from "@tanstack/react-table"; +import type { AxiosProgressEvent } from "axios"; +import { FileText, Building2, Monitor } from "lucide-react"; +import { useState } from "react"; + +interface BlackListManagerTemplateProps { + title: string; + description: string; + data: TData[]; + isLoading: boolean; + columns: ColumnDef[]; + onUpload: ( + fd: FormData, + config?: { onUploadProgress?: (e: AxiosProgressEvent) => void } + ) => Promise; + onUpdate?: (roomName: string) => void; + updateLoading?: boolean; + onTableInit?: (table: any) => void; + rooms: string[]; + devices?: string[]; +} + +export function BlackListManagerTemplate({ + title, + description, + data, + isLoading, + columns, + onUpload, + onUpdate, + updateLoading, + onTableInit, + rooms = [], + devices = [], +}: BlackListManagerTemplateProps) { + const [dialogOpen, setDialogOpen] = useState(false); + const [dialogType, setDialogType] = useState<"room" | "device" | null>(null); + + const handleUpdateAll = () => { + if (onUpdate) onUpdate("All"); + }; + + const openRoomDialog = () => { + if (rooms.length > 0 && onUpdate) { + setDialogType("room"); + setDialogOpen(true); + } + }; + + const openDeviceDialog = () => { + if (devices.length > 0 && onUpdate) { + setDialogType("device"); + setDialogOpen(true); + } + }; + + const getDialogProps = () => { + if (dialogType === "room") { + return { + title: "Chọn phòng", + description: "Chọn các phòng cần cập nhật", + icon: , + items: rooms, + }; + } + if (dialogType === "device") { + return { + title: "Chọn thiết bị", + description: "Chọn các thiết bị cần cập nhật", + icon: , + items: devices, + }; + } + return null; + }; + const dialogProps = getDialogProps(); + + return ( +
+ {/* Header */} +
+
+

{title}

+

{description}

+
+ +
+ + {/* Table */} + + + + Danh sách phần mềm bị chặn + + + Các phần mềm không được cho phép trong hệ thống + + + + + + + + {/* Footer */} + {onUpdate && ( + + + + )} + + + {dialogProps && ( + setDialogOpen(false)} + title={dialogProps.title} + description={dialogProps.description} + icon={dialogProps.icon} + items={dialogProps.items} + onConfirm={async (selectedItems) => { + if (!onUpdate) return; + for (const item of selectedItems) { + onUpdate(item); + } + setDialogOpen(false); + }} + /> + )} +
+ ); +} diff --git a/src/types/device.ts b/src/types/device.ts new file mode 100644 index 0000000..53096da --- /dev/null +++ b/src/types/device.ts @@ -0,0 +1,13 @@ +export interface NetworkInfo { + macAddress?: string; + ipAddress?: string; +} + +export interface DeviceHealthCheck { + id: string; + deviceTime: string; + version?: string; + room?: string; + isOffline: boolean; + networkInfos: NetworkInfo[]; +}