add add config file function
This commit is contained in:
parent
28c7bfc09e
commit
b903549fb9
|
|
@ -1,10 +1,21 @@
|
||||||
import { useState, useMemo } from "react";
|
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 { Input } from "@/components/ui/input";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
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 { Room } from "@/types/room";
|
||||||
import type { DeviceHealthCheck } from "@/types/device";
|
import type { DeviceHealthCheck } from "@/types/device";
|
||||||
|
|
||||||
|
|
@ -12,7 +23,7 @@ interface DeviceSearchDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
rooms: Room[];
|
rooms: Room[];
|
||||||
onSelect: (deviceIds: string[]) => void;
|
onSelect: (deviceIds: string[]) => void | Promise<void>;
|
||||||
fetchDevices: (roomName: string) => Promise<DeviceHealthCheck[]>; // API fetch
|
fetchDevices: (roomName: string) => Promise<DeviceHealthCheck[]>; // API fetch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,7 +36,9 @@ export function DeviceSearchDialog({
|
||||||
}: DeviceSearchDialogProps) {
|
}: DeviceSearchDialogProps) {
|
||||||
const [selected, setSelected] = useState<string[]>([]);
|
const [selected, setSelected] = useState<string[]>([]);
|
||||||
const [expandedRoom, setExpandedRoom] = useState<string | null>(null);
|
const [expandedRoom, setExpandedRoom] = useState<string | null>(null);
|
||||||
const [roomDevices, setRoomDevices] = useState<Record<string, DeviceHealthCheck[]>>({});
|
const [roomDevices, setRoomDevices] = useState<
|
||||||
|
Record<string, DeviceHealthCheck[]>
|
||||||
|
>({});
|
||||||
const [loadingRoom, setLoadingRoom] = useState<string | null>(null);
|
const [loadingRoom, setLoadingRoom] = useState<string | null>(null);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
|
|
@ -39,7 +52,7 @@ export function DeviceSearchDialog({
|
||||||
|
|
||||||
const filteredRooms = useMemo(() => {
|
const filteredRooms = useMemo(() => {
|
||||||
if (!searchQuery) return sortedRooms;
|
if (!searchQuery) return sortedRooms;
|
||||||
return sortedRooms.filter(room =>
|
return sortedRooms.filter((room) =>
|
||||||
room.name.toLowerCase().includes(searchQuery.toLowerCase())
|
room.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
);
|
);
|
||||||
}, [sortedRooms, searchQuery]);
|
}, [sortedRooms, searchQuery]);
|
||||||
|
|
@ -56,7 +69,7 @@ export function DeviceSearchDialog({
|
||||||
setLoadingRoom(roomName);
|
setLoadingRoom(roomName);
|
||||||
try {
|
try {
|
||||||
const devices = await fetchDevices(roomName);
|
const devices = await fetchDevices(roomName);
|
||||||
setRoomDevices(prev => ({ ...prev, [roomName]: devices }));
|
setRoomDevices((prev) => ({ ...prev, [roomName]: devices }));
|
||||||
setExpandedRoom(roomName);
|
setExpandedRoom(roomName);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch devices:", error);
|
console.error("Failed to fetch devices:", error);
|
||||||
|
|
@ -72,29 +85,36 @@ export function DeviceSearchDialog({
|
||||||
|
|
||||||
const toggleDevice = (deviceId: string) => {
|
const toggleDevice = (deviceId: string) => {
|
||||||
setSelected((prev) =>
|
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 toggleAllInRoom = (roomName: string) => {
|
||||||
const devices = roomDevices[roomName] || [];
|
const devices = roomDevices[roomName] || [];
|
||||||
const deviceIds = devices.map(d => d.id);
|
const deviceIds = devices.map((d) => d.id);
|
||||||
const allSelected = deviceIds.every(id => selected.includes(id));
|
const allSelected = deviceIds.every((id) => selected.includes(id));
|
||||||
|
|
||||||
if (allSelected) {
|
if (allSelected) {
|
||||||
setSelected(prev => prev.filter(id => !deviceIds.includes(id)));
|
setSelected((prev) => prev.filter((id) => !deviceIds.includes(id)));
|
||||||
} else {
|
} else {
|
||||||
setSelected(prev => [...new Set([...prev, ...deviceIds])]);
|
setSelected((prev) => [...new Set([...prev, ...deviceIds])]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = async () => {
|
||||||
onSelect(selected);
|
try {
|
||||||
setSelected([]);
|
await onSelect(selected);
|
||||||
setExpandedRoom(null);
|
} catch (e) {
|
||||||
setRoomDevices({});
|
console.error("Error on select:", e);
|
||||||
setSearchQuery("");
|
} finally {
|
||||||
onClose();
|
setSelected([]);
|
||||||
|
setExpandedRoom(null);
|
||||||
|
setRoomDevices({});
|
||||||
|
setSearchQuery("");
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
|
|
@ -107,7 +127,7 @@ export function DeviceSearchDialog({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleClose}>
|
<Dialog open={open} onOpenChange={handleClose}>
|
||||||
<DialogContent className="max-w-3xl">
|
<DialogContent className="max-w-none w-[95vw] max-h-[90vh]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<Monitor className="w-6 h-6 text-primary" />
|
<Monitor className="w-6 h-6 text-primary" />
|
||||||
|
|
@ -136,15 +156,22 @@ export function DeviceSearchDialog({
|
||||||
const isExpanded = expandedRoom === room.name;
|
const isExpanded = expandedRoom === room.name;
|
||||||
const isLoading = loadingRoom === room.name;
|
const isLoading = loadingRoom === room.name;
|
||||||
const devices = roomDevices[room.name] || [];
|
const devices = roomDevices[room.name] || [];
|
||||||
const allSelected = devices.length > 0 && devices.every(d => selected.includes(d.id));
|
const allSelected =
|
||||||
const someSelected = devices.some(d => selected.includes(d.id));
|
devices.length > 0 &&
|
||||||
const selectedCount = devices.filter(d => selected.includes(d.id)).length;
|
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 (
|
return (
|
||||||
<div key={room.name} className="border rounded-lg overflow-hidden">
|
<div
|
||||||
|
key={room.name}
|
||||||
|
className="border rounded-lg overflow-hidden"
|
||||||
|
>
|
||||||
{/* Room header - clickable */}
|
{/* Room header - clickable */}
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-2 p-3 hover:bg-muted/50 cursor-pointer"
|
className="flex items-center gap-1 px-2 py-1.5 hover:bg-muted/50 cursor-pointer"
|
||||||
onClick={() => handleRoomClick(room.name)}
|
onClick={() => handleRoomClick(room.name)}
|
||||||
>
|
>
|
||||||
{/* Expand icon or loading */}
|
{/* Expand icon or loading */}
|
||||||
|
|
@ -164,24 +191,28 @@ export function DeviceSearchDialog({
|
||||||
toggleAllInRoom(room.name);
|
toggleAllInRoom(room.name);
|
||||||
}}
|
}}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className={someSelected && !allSelected ? "opacity-50" : ""}
|
className={
|
||||||
|
someSelected && !allSelected ? "opacity-50" : ""
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Building2 className="w-4 h-4 text-primary flex-shrink-0" />
|
<Building2 className="w-4 h-4 text-primary flex-shrink-0" />
|
||||||
|
|
||||||
<span className="font-semibold flex-1">{room.name}</span>
|
<span className="font-semibold flex-1 text-sm">
|
||||||
|
{room.name}
|
||||||
|
</span>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
<div className="flex items-center gap-1 text-xs text-muted-foreground flex-shrink-0">
|
||||||
{selectedCount > 0 && (
|
{selectedCount > 0 && (
|
||||||
<span className="text-primary font-medium">
|
<span className="text-primary font-medium">
|
||||||
{selectedCount}/
|
{selectedCount}/
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span>{room.numberOfDevices} thiết bị</span>
|
<span>{room.numberOfDevices}</span>
|
||||||
{room.numberOfOfflineDevices > 0 && (
|
{room.numberOfOfflineDevices > 0 && (
|
||||||
<span className="text-xs px-2 py-0.5 rounded-full bg-red-100 text-red-700">
|
<span className="text-xs px-1.5 py-0.5 rounded-full bg-red-100 text-red-700">
|
||||||
{room.numberOfOfflineDevices} offline
|
{room.numberOfOfflineDevices}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -189,68 +220,74 @@ export function DeviceSearchDialog({
|
||||||
|
|
||||||
{/* Device table - collapsible */}
|
{/* Device table - collapsible */}
|
||||||
{isExpanded && devices.length > 0 && (
|
{isExpanded && devices.length > 0 && (
|
||||||
<div className="border-t bg-muted/20">
|
<div className="border-t bg-muted/20 overflow-x-auto">
|
||||||
<div className="overflow-x-auto">
|
<table className="w-full text-xs">
|
||||||
<table className="w-full text-sm">
|
<thead className="bg-muted/50 border-b sticky top-0">
|
||||||
<thead className="bg-muted/50 border-b">
|
<tr>
|
||||||
<tr>
|
<th className="w-8 px-1 py-1"></th>
|
||||||
<th className="w-12 p-2"></th>
|
<th className="text-left px-1 py-1 font-medium min-w-20 text-xs">
|
||||||
<th className="text-left p-2 font-medium">Thiết bị</th>
|
Thiết bị
|
||||||
<th className="text-left p-2 font-medium">IP Address</th>
|
</th>
|
||||||
<th className="text-left p-2 font-medium">MAC Address</th>
|
<th className="text-left px-1 py-1 font-medium min-w-24 text-xs">
|
||||||
<th className="text-left p-2 font-medium">Version</th>
|
IP
|
||||||
<th className="text-left p-2 font-medium">Trạng thái</th>
|
</th>
|
||||||
|
<th className="text-left px-1 py-1 font-medium min-w-28 text-xs">
|
||||||
|
MAC
|
||||||
|
</th>
|
||||||
|
<th className="text-left px-1 py-1 font-medium min-w-12 text-xs">
|
||||||
|
Ver
|
||||||
|
</th>
|
||||||
|
<th className="text-left px-1 py-1 font-medium min-w-16 text-xs">
|
||||||
|
Trạng thái
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{devices.map((device) => (
|
||||||
|
<tr
|
||||||
|
key={device.id}
|
||||||
|
className="border-b last:border-b-0 hover:bg-muted/50"
|
||||||
|
>
|
||||||
|
<td className="px-1 py-1">
|
||||||
|
<Checkbox
|
||||||
|
checked={selected.includes(device.id)}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
toggleDevice(device.id)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="px-1 py-1">
|
||||||
|
<div className="flex items-center gap-0.5">
|
||||||
|
<Monitor className="w-3 h-3 text-muted-foreground flex-shrink-0" />
|
||||||
|
<span className="font-mono text-xs truncate">
|
||||||
|
{device.id}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-1 py-1 font-mono text-xs truncate">
|
||||||
|
{device.networkInfos[0]?.ipAddress || "-"}
|
||||||
|
</td>
|
||||||
|
<td className="px-1 py-1 font-mono text-xs truncate">
|
||||||
|
{device.networkInfos[0]?.macAddress || "-"}
|
||||||
|
</td>
|
||||||
|
<td className="px-1 py-1 text-xs whitespace-nowrap">
|
||||||
|
{device.version ? `v${device.version}` : "-"}
|
||||||
|
</td>
|
||||||
|
<td className="px-1 py-1 text-xs">
|
||||||
|
{device.isOffline ? (
|
||||||
|
<span className="text-xs px-1 py-0.5 rounded-full bg-red-100 text-red-700 font-medium whitespace-nowrap inline-block">
|
||||||
|
Offline
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs px-1 py-0.5 rounded-full bg-green-100 text-green-700 font-medium whitespace-nowrap inline-block">
|
||||||
|
Online
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
))}
|
||||||
<tbody>
|
</tbody>
|
||||||
{devices.map((device) => (
|
</table>
|
||||||
<tr
|
|
||||||
key={device.id}
|
|
||||||
className="border-b last:border-b-0 hover:bg-muted/50"
|
|
||||||
>
|
|
||||||
<td className="p-2">
|
|
||||||
<Checkbox
|
|
||||||
checked={selected.includes(device.id)}
|
|
||||||
onCheckedChange={() => toggleDevice(device.id)}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td className="p-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Monitor className="w-3.5 h-3.5 text-muted-foreground" />
|
|
||||||
<span className="font-mono">{device.id}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="p-2 font-mono text-xs">
|
|
||||||
{device.networkInfos[0]?.ipAddress || "-"}
|
|
||||||
</td>
|
|
||||||
<td className="p-2 font-mono text-xs">
|
|
||||||
{device.networkInfos[0]?.macAddress || "-"}
|
|
||||||
</td>
|
|
||||||
<td className="p-2">
|
|
||||||
{device.version ? (
|
|
||||||
<span className="text-xs px-2 py-0.5 rounded-full bg-muted">
|
|
||||||
v{device.version}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
"-"
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="p-2">
|
|
||||||
{device.isOffline ? (
|
|
||||||
<span className="text-xs px-2 py-0.5 rounded-full bg-red-100 text-red-700 font-medium">
|
|
||||||
Offline
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-xs px-2 py-0.5 rounded-full bg-green-100 text-green-700 font-medium">
|
|
||||||
Online
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -261,17 +298,24 @@ export function DeviceSearchDialog({
|
||||||
|
|
||||||
{/* Selected count */}
|
{/* Selected count */}
|
||||||
{selected.length > 0 && (
|
{selected.length > 0 && (
|
||||||
<div className="text-sm text-muted-foreground bg-muted/50 px-3 py-2 rounded">
|
<div className="text-xs text-muted-foreground bg-muted/50 px-2 py-1.5 rounded">
|
||||||
Đã chọn: <span className="font-semibold text-foreground">{selected.length}</span> thiết bị
|
Đã chọn:{" "}
|
||||||
|
<span className="font-semibold text-foreground">
|
||||||
|
{selected.length}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button variant="outline" onClick={handleClose}>
|
<Button variant="outline" onClick={handleClose} size="sm">
|
||||||
Hủy
|
Hủy
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleConfirm} disabled={selected.length === 0}>
|
<Button
|
||||||
|
onClick={handleConfirm}
|
||||||
|
disabled={selected.length === 0}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
Xác nhận ({selected.length})
|
Xác nhận ({selected.length})
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,8 @@ export function SelectDialog({
|
||||||
const handleConfirm = async () => {
|
const handleConfirm = async () => {
|
||||||
await onConfirm(selected);
|
await onConfirm(selected);
|
||||||
setSelected([]);
|
setSelected([]);
|
||||||
|
setSearch("");
|
||||||
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,11 @@ export function DeviceGrid({ devices }: { devices: any[] }) {
|
||||||
{Array.from({ length: 4 }).map((_, i) => {
|
{Array.from({ length: 4 }).map((_, i) => {
|
||||||
const pos = leftStart + (3 - i);
|
const pos = leftStart + (3 - i);
|
||||||
return (
|
return (
|
||||||
<ComputerCard key={pos} device={deviceMap.get(pos)} position={pos} />
|
<ComputerCard
|
||||||
|
key={pos}
|
||||||
|
device={deviceMap.get(pos)}
|
||||||
|
position={pos}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
|
@ -37,7 +41,11 @@ export function DeviceGrid({ devices }: { devices: any[] }) {
|
||||||
{Array.from({ length: 4 }).map((_, i) => {
|
{Array.from({ length: 4 }).map((_, i) => {
|
||||||
const pos = rightStart + (3 - i);
|
const pos = rightStart + (3 - i);
|
||||||
return (
|
return (
|
||||||
<ComputerCard key={pos} device={deviceMap.get(pos)} position={pos} />
|
<ComputerCard
|
||||||
|
key={pos}
|
||||||
|
device={deviceMap.get(pos)}
|
||||||
|
position={pos}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -46,6 +54,9 @@ export function DeviceGrid({ devices }: { devices: any[] }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-0.5 py-8 space-y-6">
|
<div className="px-0.5 py-8 space-y-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{Array.from({ length: totalRows }).map((_, i) => renderRow(i))}
|
||||||
|
</div>
|
||||||
<div className="flex items-center justify-between px-4 pb-6 border-b-2 border-dashed">
|
<div className="flex items-center justify-between px-4 pb-6 border-b-2 border-dashed">
|
||||||
<div className="flex items-center gap-3 px-6 py-4 bg-muted rounded-lg border-2 border-border">
|
<div className="flex items-center gap-3 px-6 py-4 bg-muted rounded-lg border-2 border-border">
|
||||||
<DoorOpen className="h-6 w-6 text-muted-foreground" />
|
<DoorOpen className="h-6 w-6 text-muted-foreground" />
|
||||||
|
|
@ -56,9 +67,6 @@ export function DeviceGrid({ devices }: { devices: any[] }) {
|
||||||
<span className="font-semibold text-lg">Bàn Giảng Viên</span>
|
<span className="font-semibold text-lg">Bàn Giảng Viên</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
|
||||||
{Array.from({ length: totalRows }).map((_, i) => renderRow(i))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Loader2, RefreshCw, ChevronDown } from "lucide-react";
|
import { Loader2, RefreshCw, ChevronDown } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
interface RequestUpdateMenuProps {
|
interface RequestUpdateMenuProps {
|
||||||
onUpdateDevice: () => void;
|
onUpdateDevice: () => void;
|
||||||
|
|
@ -21,8 +22,33 @@ export function RequestUpdateMenu({
|
||||||
onUpdateAll,
|
onUpdateAll,
|
||||||
loading,
|
loading,
|
||||||
}: RequestUpdateMenuProps) {
|
}: 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 (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
@ -45,17 +71,17 @@ export function RequestUpdateMenu({
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className="w-56">
|
<DropdownMenuContent align="start" className="w-56">
|
||||||
<DropdownMenuItem onClick={onUpdateDevice} disabled={loading}>
|
<DropdownMenuItem onClick={handleUpdateDevice} disabled={loading}>
|
||||||
<RefreshCw className="h-4 w-4 mr-2" />
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
<span>Cập nhật thiết bị cụ thể</span>
|
<span>Cập nhật thiết bị cụ thể</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={onUpdateRoom} disabled={loading}>
|
<DropdownMenuItem onClick={handleUpdateRoom} disabled={loading}>
|
||||||
<RefreshCw className="h-4 w-4 mr-2" />
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
<span>Cập nhật theo phòng</span>
|
<span>Cập nhật theo phòng</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={onUpdateAll} disabled={loading}>
|
<DropdownMenuItem onClick={handleUpdateAll} disabled={loading}>
|
||||||
<RefreshCw className="h-4 w-4 mr-2" />
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
<span>Cập nhật tất cả thiết bị</span>
|
<span>Cập nhật tất cả thiết bị</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@ export const API_ENDPOINTS = {
|
||||||
REQUEST_UPDATE_BLACKLIST: `${BASE_URL}/AppVersion/blacklist/request-update`,
|
REQUEST_UPDATE_BLACKLIST: `${BASE_URL}/AppVersion/blacklist/request-update`,
|
||||||
},
|
},
|
||||||
DEVICE_COMM: {
|
DEVICE_COMM: {
|
||||||
DOWNLOAD_MSI: (roomName: string) => `${BASE_URL}/DeviceComm/installmsi/${roomName}`,
|
DOWNLOAD_FILES: (roomName: string) => `${BASE_URL}/DeviceComm/downloadfile/${roomName}`,
|
||||||
|
INSTALL_MSI: (roomName: string) => `${BASE_URL}/DeviceComm/installmsi/${roomName}`,
|
||||||
GET_ALL_DEVICES: `${BASE_URL}/DeviceComm/alldevices`,
|
GET_ALL_DEVICES: `${BASE_URL}/DeviceComm/alldevices`,
|
||||||
GET_ROOM_LIST: `${BASE_URL}/DeviceComm/rooms`,
|
GET_ROOM_LIST: `${BASE_URL}/DeviceComm/rooms`,
|
||||||
GET_DEVICE_FROM_ROOM: (roomName: string) =>
|
GET_DEVICE_FROM_ROOM: (roomName: string) =>
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,6 @@ function AgentsPage() {
|
||||||
? [data]
|
? [data]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// Mutation upload
|
|
||||||
const uploadMutation = useMutationData<FormData>({
|
const uploadMutation = useMutationData<FormData>({
|
||||||
url: BASE_URL + API_ENDPOINTS.APP_VERSION.UPLOAD,
|
url: BASE_URL + API_ENDPOINTS.APP_VERSION.UPLOAD,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
@ -56,7 +55,10 @@ function AgentsPage() {
|
||||||
url: "",
|
url: "",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
onSuccess: () => toast.success("Đã gửi yêu cầu update!"),
|
onSuccess: () => toast.success("Đã gửi yêu cầu update!"),
|
||||||
onError: () => toast.error("Gửi yêu cầu thất bại!"),
|
onError: (error) => {
|
||||||
|
console.error("Update mutation error:", error);
|
||||||
|
toast.error("Gửi yêu cầu thất bại!");
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleUpload = async (
|
const handleUpload = async (
|
||||||
|
|
|
||||||
|
|
@ -55,13 +55,23 @@ function AppsComponent() {
|
||||||
const installMutation = useMutationData<{ MsiFileIds: number[] }>({
|
const installMutation = useMutationData<{ MsiFileIds: number[] }>({
|
||||||
url: "",
|
url: "",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
onSuccess: () => toast.success("Đã gửi yêu cầu cài đặt MSI!"),
|
onSuccess: () => toast.success("Đã gửi yêu cầu cài đặt file!"),
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error("Install error:", error);
|
console.error("Install error:", error);
|
||||||
toast.error("Gửi yêu cầu thất bại!");
|
toast.error("Gửi yêu cầu thất bại!");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const downloadMutation = useMutationData<{ MsiFileIds: number[] }>({
|
||||||
|
url: "",
|
||||||
|
method: "POST",
|
||||||
|
onSuccess: () => toast.success("Đã gửi yêu cầu tải file!"),
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Download error:", error);
|
||||||
|
toast.error("Gửi yêu cầu thất bại!");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Cột bảng
|
// Cột bảng
|
||||||
const columns: ColumnDef<Version>[] = [
|
const columns: ColumnDef<Version>[] = [
|
||||||
{ accessorKey: "version", header: "Phiên bản" },
|
{ accessorKey: "version", header: "Phiên bản" },
|
||||||
|
|
@ -124,7 +134,34 @@ function AppsComponent() {
|
||||||
try {
|
try {
|
||||||
for (const roomName of roomNames) {
|
for (const roomName of roomNames) {
|
||||||
await installMutation.mutateAsync({
|
await installMutation.mutateAsync({
|
||||||
url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.DOWNLOAD_MSI(roomName),
|
url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.INSTALL_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!");
|
||||||
|
} catch (e) {
|
||||||
|
toast.error("Có lỗi xảy ra khi cài đặt!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDonwload = async (roomNames: string[]) => {
|
||||||
|
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 để cài đặt!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MsiFileIds = selectedRows.map((row: any) => row.original.id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const roomName of roomNames) {
|
||||||
|
await downloadMutation.mutateAsync({
|
||||||
|
url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.DOWNLOAD_FILES(roomName),
|
||||||
data: { MsiFileIds },
|
data: { MsiFileIds },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -137,13 +174,15 @@ function AppsComponent() {
|
||||||
return (
|
return (
|
||||||
<AppManagerTemplate<Version>
|
<AppManagerTemplate<Version>
|
||||||
title="Quản lý phần mềm"
|
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 MSI"
|
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}
|
data={versionList}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
onUpload={handleUpload}
|
onUpload={handleUpload}
|
||||||
onUpdate={handleInstall}
|
onUpdate={handleInstall}
|
||||||
|
onDownload={handleDonwload}
|
||||||
updateLoading={installMutation.isPending}
|
updateLoading={installMutation.isPending}
|
||||||
|
downloadLoading={downloadMutation.isPending}
|
||||||
onTableInit={setTable}
|
onTableInit={setTable}
|
||||||
rooms={roomData}
|
rooms={roomData}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import { useMutationData } from "@/hooks/useMutationData";
|
||||||
import { useQueryData } from "@/hooks/useQueryData";
|
import { useQueryData } from "@/hooks/useQueryData";
|
||||||
import { BASE_URL, API_ENDPOINTS } from "@/config/api";
|
import { BASE_URL, API_ENDPOINTS } from "@/config/api";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type { Room } from "@/types/room";
|
|
||||||
|
|
||||||
type SendCommandRequest = { Command: string };
|
type SendCommandRequest = { Command: string };
|
||||||
type SendCommandResponse = { status: string; message: string };
|
type SendCommandResponse = { status: string; message: string };
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ interface AppManagerTemplateProps<TData> {
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
onUpdate?: (targetNames: string[]) => Promise<void> | void;
|
onUpdate?: (targetNames: string[]) => Promise<void> | void;
|
||||||
updateLoading?: boolean;
|
updateLoading?: boolean;
|
||||||
|
onDownload?: (targetNames: string[]) => Promise<void> | void;
|
||||||
|
downloadLoading?: boolean;
|
||||||
onTableInit?: (table: any) => void;
|
onTableInit?: (table: any) => void;
|
||||||
rooms?: Room[];
|
rooms?: Room[];
|
||||||
devices?: string[];
|
devices?: string[];
|
||||||
|
|
@ -46,12 +48,14 @@ export function AppManagerTemplate<TData>({
|
||||||
onUpload,
|
onUpload,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
updateLoading,
|
updateLoading,
|
||||||
|
onDownload,
|
||||||
|
downloadLoading,
|
||||||
onTableInit,
|
onTableInit,
|
||||||
rooms = [],
|
rooms = [],
|
||||||
devices = [],
|
devices = [],
|
||||||
}: AppManagerTemplateProps<TData>) {
|
}: AppManagerTemplateProps<TData>) {
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [dialogType, setDialogType] = useState<"room" | "device" | null>(null);
|
const [dialogType, setDialogType] = useState<"room" | "device" | "download-room" | "download-device" | null>(null);
|
||||||
|
|
||||||
const openRoomDialog = () => {
|
const openRoomDialog = () => {
|
||||||
if (rooms.length > 0 && onUpdate) {
|
if (rooms.length > 0 && onUpdate) {
|
||||||
|
|
@ -67,14 +71,31 @@ export function AppManagerTemplate<TData>({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openDownloadRoomDialog = () => {
|
||||||
|
if (rooms.length > 0 && onDownload) {
|
||||||
|
setDialogType("download-room");
|
||||||
|
setDialogOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDownloadDeviceDialog = () => {
|
||||||
|
if (onDownload) {
|
||||||
|
setDialogType("download-device");
|
||||||
|
setDialogOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleUpdateAll = async () => {
|
const handleUpdateAll = async () => {
|
||||||
if (!onUpdate) return;
|
if (!onUpdate) return;
|
||||||
// Assuming Room has an 'id' property; adjust if needed
|
try {
|
||||||
const roomIds = rooms.map((room) =>
|
const roomIds = rooms.map((room) =>
|
||||||
typeof room === "string" ? room : room.name
|
typeof room === "string" ? room : room.name
|
||||||
);
|
);
|
||||||
const allTargets = [...roomIds, ...devices];
|
const allTargets = [...roomIds, ...devices];
|
||||||
await onUpdate(allTargets);
|
await onUpdate(allTargets);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Update error:", e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -113,13 +134,28 @@ export function AppManagerTemplate<TData>({
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
{onUpdate && (
|
{onUpdate && (
|
||||||
<CardFooter>
|
<CardFooter className="gap-2">
|
||||||
<RequestUpdateMenu
|
<RequestUpdateMenu
|
||||||
onUpdateDevice={openDeviceDialog}
|
onUpdateDevice={openDeviceDialog}
|
||||||
onUpdateRoom={openRoomDialog}
|
onUpdateRoom={openRoomDialog}
|
||||||
onUpdateAll={handleUpdateAll}
|
onUpdateAll={handleUpdateAll}
|
||||||
loading={updateLoading}
|
loading={updateLoading}
|
||||||
/>
|
/>
|
||||||
|
{onDownload && (
|
||||||
|
<RequestUpdateMenu
|
||||||
|
onUpdateDevice={openDownloadDeviceDialog}
|
||||||
|
onUpdateRoom={openDownloadRoomDialog}
|
||||||
|
onUpdateAll={() => {
|
||||||
|
if (!onDownload) return;
|
||||||
|
const roomIds = rooms.map((room) =>
|
||||||
|
typeof room === "string" ? room : room.name
|
||||||
|
);
|
||||||
|
const allTargets = [...roomIds, ...devices];
|
||||||
|
onDownload(allTargets);
|
||||||
|
}}
|
||||||
|
loading={downloadLoading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
@ -128,15 +164,25 @@ export function AppManagerTemplate<TData>({
|
||||||
{dialogType === "room" && (
|
{dialogType === "room" && (
|
||||||
<SelectDialog
|
<SelectDialog
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
onClose={() => setDialogOpen(false)}
|
onClose={() => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
setDialogType(null);
|
||||||
|
}}
|
||||||
title="Chọn phòng"
|
title="Chọn phòng"
|
||||||
description="Chọn các phòng cần cập nhật"
|
description="Chọn các phòng cần cập nhật"
|
||||||
icon={<Building2 className="w-6 h-6 text-primary" />}
|
icon={<Building2 className="w-6 h-6 text-primary" />}
|
||||||
items={mapRoomsToSelectItems(rooms)}
|
items={mapRoomsToSelectItems(rooms)}
|
||||||
onConfirm={async (selectedItems) => {
|
onConfirm={async (selectedItems) => {
|
||||||
if (!onUpdate) return;
|
if (!onUpdate) return;
|
||||||
await onUpdate(selectedItems);
|
try {
|
||||||
setDialogOpen(false);
|
await onUpdate(selectedItems);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Update error:", e);
|
||||||
|
} finally {
|
||||||
|
setDialogOpen(false);
|
||||||
|
setDialogType(null);
|
||||||
|
setTimeout(() => window.location.reload(), 500);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
@ -145,10 +191,84 @@ export function AppManagerTemplate<TData>({
|
||||||
{dialogType === "device" && (
|
{dialogType === "device" && (
|
||||||
<DeviceSearchDialog
|
<DeviceSearchDialog
|
||||||
open={dialogOpen && dialogType === "device"}
|
open={dialogOpen && dialogType === "device"}
|
||||||
onClose={() => setDialogOpen(false)}
|
onClose={() => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
setDialogType(null);
|
||||||
|
}}
|
||||||
rooms={rooms}
|
rooms={rooms}
|
||||||
fetchDevices={fetchDevicesFromRoom} // ⬅ thêm vào đây
|
fetchDevices={fetchDevicesFromRoom}
|
||||||
onSelect={(deviceIds) => onUpdate && onUpdate(deviceIds)}
|
onSelect={async (deviceIds) => {
|
||||||
|
if (!onUpdate) {
|
||||||
|
setDialogOpen(false);
|
||||||
|
setDialogType(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await onUpdate(deviceIds);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Update error:", e);
|
||||||
|
} finally {
|
||||||
|
setDialogOpen(false);
|
||||||
|
setDialogType(null);
|
||||||
|
setTimeout(() => window.location.reload(), 500);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Dialog tải file - chọn phòng */}
|
||||||
|
{dialogType === "download-room" && (
|
||||||
|
<SelectDialog
|
||||||
|
open={dialogOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
setDialogType(null);
|
||||||
|
}}
|
||||||
|
title="Chọn phòng"
|
||||||
|
description="Chọn các phòng để tải file xuống"
|
||||||
|
icon={<Building2 className="w-6 h-6 text-primary" />}
|
||||||
|
items={mapRoomsToSelectItems(rooms)}
|
||||||
|
onConfirm={async (selectedItems) => {
|
||||||
|
if (!onDownload) return;
|
||||||
|
try {
|
||||||
|
await onDownload(selectedItems);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Download error:", e);
|
||||||
|
} finally {
|
||||||
|
setDialogOpen(false);
|
||||||
|
setDialogType(null);
|
||||||
|
setTimeout(() => window.location.reload(), 500);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Dialog tải file - tìm thiết bị */}
|
||||||
|
{dialogType === "download-device" && (
|
||||||
|
<DeviceSearchDialog
|
||||||
|
open={dialogOpen && dialogType === "download-device"}
|
||||||
|
onClose={() => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
setDialogType(null);
|
||||||
|
}}
|
||||||
|
rooms={rooms}
|
||||||
|
fetchDevices={fetchDevicesFromRoom}
|
||||||
|
onSelect={async (deviceIds) => {
|
||||||
|
if (!onDownload) {
|
||||||
|
setDialogOpen(false);
|
||||||
|
setDialogType(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await onDownload(deviceIds);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Download error:", e);
|
||||||
|
} finally {
|
||||||
|
setDialogOpen(false);
|
||||||
|
setDialogType(null);
|
||||||
|
setTimeout(() => window.location.reload(), 500);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -59,14 +59,18 @@ export function FormSubmitTemplate({
|
||||||
};
|
};
|
||||||
|
|
||||||
// Gửi cho tất cả
|
// Gửi cho tất cả
|
||||||
const handleSubmitAll = () => {
|
const handleSubmitAll = async () => {
|
||||||
if (!onSubmit) return;
|
if (!onSubmit) return;
|
||||||
const roomIds = rooms.map((room) =>
|
try {
|
||||||
typeof room === "string" ? room : room.name
|
const roomIds = rooms.map((room) =>
|
||||||
);
|
typeof room === "string" ? room : room.name
|
||||||
const allTargets = [...roomIds, ...devices];
|
);
|
||||||
for (const target of allTargets) {
|
const allTargets = [...roomIds, ...devices];
|
||||||
onSubmit(target, command);
|
for (const target of allTargets) {
|
||||||
|
await onSubmit(target, command);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Submit error:", e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -103,17 +107,27 @@ export function FormSubmitTemplate({
|
||||||
{dialogType === "room" && (
|
{dialogType === "room" && (
|
||||||
<SelectDialog
|
<SelectDialog
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
onClose={() => setDialogOpen(false)}
|
onClose={() => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
setDialogType(null);
|
||||||
|
}}
|
||||||
title="Chọn phòng để gửi lệnh"
|
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"
|
description="Chọn các phòng muốn gửi lệnh CMD tới"
|
||||||
icon={<Building2 className="w-6 h-6 text-primary" />}
|
icon={<Building2 className="w-6 h-6 text-primary" />}
|
||||||
items={mapRoomsToSelectItems(rooms)}
|
items={mapRoomsToSelectItems(rooms)}
|
||||||
onConfirm={async (selectedItems) => {
|
onConfirm={async (selectedItems) => {
|
||||||
if (!onSubmit) return;
|
if (!onSubmit) return;
|
||||||
for (const item of selectedItems) {
|
try {
|
||||||
await onSubmit(item, command);
|
for (const item of selectedItems) {
|
||||||
|
await onSubmit(item, command);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Submit error:", e);
|
||||||
|
} finally {
|
||||||
|
setDialogOpen(false);
|
||||||
|
setDialogType(null);
|
||||||
|
setTimeout(() => window.location.reload(), 500);
|
||||||
}
|
}
|
||||||
setDialogOpen(false);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
@ -122,13 +136,26 @@ export function FormSubmitTemplate({
|
||||||
{dialogType === "device" && (
|
{dialogType === "device" && (
|
||||||
<DeviceSearchDialog
|
<DeviceSearchDialog
|
||||||
open={dialogOpen && dialogType === "device"}
|
open={dialogOpen && dialogType === "device"}
|
||||||
onClose={() => setDialogOpen(false)}
|
onClose={() => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
setDialogType(null);
|
||||||
|
}}
|
||||||
rooms={rooms}
|
rooms={rooms}
|
||||||
fetchDevices={fetchDevicesFromRoom} // ⬅ thêm vào đây
|
fetchDevices={fetchDevicesFromRoom}
|
||||||
onSelect={(deviceIds) =>
|
onSelect={async (deviceIds) => {
|
||||||
onSubmit &&
|
if (!onSubmit) return;
|
||||||
deviceIds.forEach((deviceId) => onSubmit(deviceId, command))
|
try {
|
||||||
}
|
for (const deviceId of deviceIds) {
|
||||||
|
await onSubmit(deviceId, command);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Submit error:", e);
|
||||||
|
} finally {
|
||||||
|
setDialogOpen(false);
|
||||||
|
setDialogType(null);
|
||||||
|
setTimeout(() => window.location.reload(), 500);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user