From b903549fb90dab05598b7b5867cd06d485577a06 Mon Sep 17 00:00:00 2001 From: phuongdm Date: Wed, 26 Nov 2025 13:16:32 +0700 Subject: [PATCH] add add config file function --- src/components/bars/device-searchbar.tsx | 236 ++++++++++++-------- src/components/dialogs/select-dialog.tsx | 2 + src/components/grids/device-grid.tsx | 18 +- src/components/menu/request-update-menu.tsx | 34 ++- src/config/api.ts | 3 +- src/routes/_authenticated/agent/index.tsx | 6 +- src/routes/_authenticated/apps/index.tsx | 45 +++- src/routes/_authenticated/command/index.tsx | 1 - src/template/app-manager-template.tsx | 148 ++++++++++-- src/template/form-submit-template.tsx | 61 +++-- 10 files changed, 411 insertions(+), 143 deletions(-) diff --git a/src/components/bars/device-searchbar.tsx b/src/components/bars/device-searchbar.tsx index 79dd3ef..ed02a93 100644 --- a/src/components/bars/device-searchbar.tsx +++ b/src/components/bars/device-searchbar.tsx @@ -1,10 +1,21 @@ import { useState, useMemo } from "react"; -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; -import { Building2, Monitor, ChevronDown, ChevronRight, Loader2 } from "lucide-react"; +import { + Building2, + Monitor, + ChevronDown, + ChevronRight, + Loader2, +} from "lucide-react"; import type { Room } from "@/types/room"; import type { DeviceHealthCheck } from "@/types/device"; @@ -12,7 +23,7 @@ interface DeviceSearchDialogProps { open: boolean; onClose: () => void; rooms: Room[]; - onSelect: (deviceIds: string[]) => void; + onSelect: (deviceIds: string[]) => void | Promise; fetchDevices: (roomName: string) => Promise; // API fetch } @@ -25,7 +36,9 @@ export function DeviceSearchDialog({ }: DeviceSearchDialogProps) { const [selected, setSelected] = useState([]); const [expandedRoom, setExpandedRoom] = useState(null); - const [roomDevices, setRoomDevices] = useState>({}); + const [roomDevices, setRoomDevices] = useState< + Record + >({}); const [loadingRoom, setLoadingRoom] = useState(null); const [searchQuery, setSearchQuery] = useState(""); @@ -39,7 +52,7 @@ export function DeviceSearchDialog({ const filteredRooms = useMemo(() => { if (!searchQuery) return sortedRooms; - return sortedRooms.filter(room => + return sortedRooms.filter((room) => room.name.toLowerCase().includes(searchQuery.toLowerCase()) ); }, [sortedRooms, searchQuery]); @@ -56,7 +69,7 @@ export function DeviceSearchDialog({ setLoadingRoom(roomName); try { const devices = await fetchDevices(roomName); - setRoomDevices(prev => ({ ...prev, [roomName]: devices })); + setRoomDevices((prev) => ({ ...prev, [roomName]: devices })); setExpandedRoom(roomName); } catch (error) { console.error("Failed to fetch devices:", error); @@ -72,29 +85,36 @@ export function DeviceSearchDialog({ const toggleDevice = (deviceId: string) => { setSelected((prev) => - prev.includes(deviceId) ? prev.filter((id) => id !== deviceId) : [...prev, deviceId] + prev.includes(deviceId) + ? prev.filter((id) => id !== deviceId) + : [...prev, deviceId] ); }; const toggleAllInRoom = (roomName: string) => { const devices = roomDevices[roomName] || []; - const deviceIds = devices.map(d => d.id); - const allSelected = deviceIds.every(id => selected.includes(id)); + const deviceIds = devices.map((d) => d.id); + const allSelected = deviceIds.every((id) => selected.includes(id)); if (allSelected) { - setSelected(prev => prev.filter(id => !deviceIds.includes(id))); + setSelected((prev) => prev.filter((id) => !deviceIds.includes(id))); } else { - setSelected(prev => [...new Set([...prev, ...deviceIds])]); + setSelected((prev) => [...new Set([...prev, ...deviceIds])]); } }; - const handleConfirm = () => { - onSelect(selected); - setSelected([]); - setExpandedRoom(null); - setRoomDevices({}); - setSearchQuery(""); - onClose(); + const handleConfirm = async () => { + try { + await onSelect(selected); + } catch (e) { + console.error("Error on select:", e); + } finally { + setSelected([]); + setExpandedRoom(null); + setRoomDevices({}); + setSearchQuery(""); + onClose(); + } }; const handleClose = () => { @@ -107,7 +127,7 @@ export function DeviceSearchDialog({ return ( - + @@ -136,15 +156,22 @@ export function DeviceSearchDialog({ const isExpanded = expandedRoom === room.name; const isLoading = loadingRoom === room.name; const devices = roomDevices[room.name] || []; - const allSelected = devices.length > 0 && devices.every(d => selected.includes(d.id)); - const someSelected = devices.some(d => selected.includes(d.id)); - const selectedCount = devices.filter(d => selected.includes(d.id)).length; + const allSelected = + devices.length > 0 && + devices.every((d) => selected.includes(d.id)); + const someSelected = devices.some((d) => selected.includes(d.id)); + const selectedCount = devices.filter((d) => + selected.includes(d.id) + ).length; return ( -
+
{/* Room header - clickable */}
handleRoomClick(room.name)} > {/* Expand icon or loading */} @@ -164,24 +191,28 @@ export function DeviceSearchDialog({ toggleAllInRoom(room.name); }} onClick={(e) => e.stopPropagation()} - className={someSelected && !allSelected ? "opacity-50" : ""} + className={ + someSelected && !allSelected ? "opacity-50" : "" + } /> )} - {room.name} + + {room.name} + -
+
{selectedCount > 0 && ( {selectedCount}/ )} - {room.numberOfDevices} thiết bị + {room.numberOfDevices} {room.numberOfOfflineDevices > 0 && ( - - {room.numberOfOfflineDevices} offline + + {room.numberOfOfflineDevices} )}
@@ -189,68 +220,74 @@ export function DeviceSearchDialog({ {/* Device table - collapsible */} {isExpanded && devices.length > 0 && ( -
-
- - - - - - - - - +
+
Thiết bịIP AddressMAC AddressVersionTrạng thái
+ + + + + + + + + + + + {devices.map((device) => ( + + + + + + + - - - {devices.map((device) => ( - - - - - - - - - ))} - -
+ Thiết bị + + IP + + MAC + + Ver + + Trạng thái +
+ + toggleDevice(device.id) + } + /> + +
+ + + {device.id} + +
+
+ {device.networkInfos[0]?.ipAddress || "-"} + + {device.networkInfos[0]?.macAddress || "-"} + + {device.version ? `v${device.version}` : "-"} + + {device.isOffline ? ( + + Offline + + ) : ( + + Online + + )} +
- toggleDevice(device.id)} - /> - -
- - {device.id} -
-
- {device.networkInfos[0]?.ipAddress || "-"} - - {device.networkInfos[0]?.macAddress || "-"} - - {device.version ? ( - - v{device.version} - - ) : ( - "-" - )} - - {device.isOffline ? ( - - Offline - - ) : ( - - Online - - )} -
-
+ ))} + +
)}
@@ -261,21 +298,28 @@ export function DeviceSearchDialog({ {/* Selected count */} {selected.length > 0 && ( -
- Đã chọn: {selected.length} thiết bị +
+ Đã chọn:{" "} + + {selected.length} +
)} {/* Actions */}
- -
); -} \ No newline at end of file +} diff --git a/src/components/dialogs/select-dialog.tsx b/src/components/dialogs/select-dialog.tsx index 235be70..3da79c1 100644 --- a/src/components/dialogs/select-dialog.tsx +++ b/src/components/dialogs/select-dialog.tsx @@ -46,6 +46,8 @@ export function SelectDialog({ const handleConfirm = async () => { await onConfirm(selected); setSelected([]); + setSearch(""); + onClose(); }; return ( diff --git a/src/components/grids/device-grid.tsx b/src/components/grids/device-grid.tsx index 62be605..d7d6a87 100644 --- a/src/components/grids/device-grid.tsx +++ b/src/components/grids/device-grid.tsx @@ -24,7 +24,11 @@ export function DeviceGrid({ devices }: { devices: any[] }) { {Array.from({ length: 4 }).map((_, i) => { const pos = leftStart + (3 - i); return ( - + ); })} @@ -37,7 +41,11 @@ export function DeviceGrid({ devices }: { devices: any[] }) { {Array.from({ length: 4 }).map((_, i) => { const pos = rightStart + (3 - i); return ( - + ); })} @@ -46,6 +54,9 @@ export function DeviceGrid({ devices }: { devices: any[] }) { return (
+
+ {Array.from({ length: totalRows }).map((_, i) => renderRow(i))} +
@@ -56,9 +67,6 @@ export function DeviceGrid({ devices }: { devices: any[] }) { Bàn Giảng Viên
-
- {Array.from({ length: totalRows }).map((_, i) => renderRow(i))} -
); } diff --git a/src/components/menu/request-update-menu.tsx b/src/components/menu/request-update-menu.tsx index ef51d71..d356b4d 100644 --- a/src/components/menu/request-update-menu.tsx +++ b/src/components/menu/request-update-menu.tsx @@ -7,6 +7,7 @@ import { DropdownMenuSeparator, } from "@/components/ui/dropdown-menu"; import { Loader2, RefreshCw, ChevronDown } from "lucide-react"; +import { useState } from "react"; interface RequestUpdateMenuProps { onUpdateDevice: () => void; @@ -21,8 +22,33 @@ export function RequestUpdateMenu({ onUpdateAll, loading, }: RequestUpdateMenuProps) { + const [open, setOpen] = useState(false); + + const handleUpdateDevice = async () => { + try { + await onUpdateDevice(); + } finally { + setOpen(false); + } + }; + + const handleUpdateRoom = async () => { + try { + await onUpdateRoom(); + } finally { + setOpen(false); + } + }; + + const handleUpdateAll = async () => { + try { + await onUpdateAll(); + } finally { + setOpen(false); + } + }; return ( - +