master #1
|
|
@ -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<void>;
|
||||
fetchDevices: (roomName: string) => Promise<DeviceHealthCheck[]>; // API fetch
|
||||
}
|
||||
|
||||
|
|
@ -25,7 +36,9 @@ export function DeviceSearchDialog({
|
|||
}: DeviceSearchDialogProps) {
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
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 [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 (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogContent className="max-w-none w-[95vw] max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Monitor className="w-6 h-6 text-primary" />
|
||||
|
|
@ -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 (
|
||||
<div key={room.name} className="border rounded-lg overflow-hidden">
|
||||
<div
|
||||
key={room.name}
|
||||
className="border rounded-lg overflow-hidden"
|
||||
>
|
||||
{/* Room header - clickable */}
|
||||
<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)}
|
||||
>
|
||||
{/* 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" : ""
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<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 && (
|
||||
<span className="text-primary font-medium">
|
||||
{selectedCount}/
|
||||
</span>
|
||||
)}
|
||||
<span>{room.numberOfDevices} thiết bị</span>
|
||||
<span>{room.numberOfDevices}</span>
|
||||
{room.numberOfOfflineDevices > 0 && (
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-red-100 text-red-700">
|
||||
{room.numberOfOfflineDevices} offline
|
||||
<span className="text-xs px-1.5 py-0.5 rounded-full bg-red-100 text-red-700">
|
||||
{room.numberOfOfflineDevices}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -189,68 +220,74 @@ export function DeviceSearchDialog({
|
|||
|
||||
{/* Device table - collapsible */}
|
||||
{isExpanded && devices.length > 0 && (
|
||||
<div className="border-t bg-muted/20">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-muted/50 border-b">
|
||||
<tr>
|
||||
<th className="w-12 p-2"></th>
|
||||
<th className="text-left p-2 font-medium">Thiết bị</th>
|
||||
<th className="text-left p-2 font-medium">IP Address</th>
|
||||
<th className="text-left p-2 font-medium">MAC Address</th>
|
||||
<th className="text-left p-2 font-medium">Version</th>
|
||||
<th className="text-left p-2 font-medium">Trạng thái</th>
|
||||
<div className="border-t bg-muted/20 overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-muted/50 border-b sticky top-0">
|
||||
<tr>
|
||||
<th className="w-8 px-1 py-1"></th>
|
||||
<th className="text-left px-1 py-1 font-medium min-w-20 text-xs">
|
||||
Thiết bị
|
||||
</th>
|
||||
<th className="text-left px-1 py-1 font-medium min-w-24 text-xs">
|
||||
IP
|
||||
</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>
|
||||
</thead>
|
||||
<tbody>
|
||||
{devices.map((device) => (
|
||||
<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>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -261,21 +298,28 @@ export function DeviceSearchDialog({
|
|||
|
||||
{/* Selected count */}
|
||||
{selected.length > 0 && (
|
||||
<div className="text-sm text-muted-foreground bg-muted/50 px-3 py-2 rounded">
|
||||
Đã chọn: <span className="font-semibold text-foreground">{selected.length}</span> thiết bị
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
<Button variant="outline" onClick={handleClose} size="sm">
|
||||
Hủy
|
||||
</Button>
|
||||
<Button onClick={handleConfirm} disabled={selected.length === 0}>
|
||||
<Button
|
||||
onClick={handleConfirm}
|
||||
disabled={selected.length === 0}
|
||||
size="sm"
|
||||
>
|
||||
Xác nhận ({selected.length})
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ export function SelectDialog({
|
|||
const handleConfirm = async () => {
|
||||
await onConfirm(selected);
|
||||
setSelected([]);
|
||||
setSearch("");
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -24,7 +24,11 @@ export function DeviceGrid({ devices }: { devices: any[] }) {
|
|||
{Array.from({ length: 4 }).map((_, i) => {
|
||||
const pos = leftStart + (3 - i);
|
||||
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) => {
|
||||
const pos = rightStart + (3 - i);
|
||||
return (
|
||||
<ComputerCard key={pos} device={deviceMap.get(pos)} position={pos} />
|
||||
<ComputerCard
|
||||
key={pos}
|
||||
device={deviceMap.get(pos)}
|
||||
position={pos}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -46,6 +54,9 @@ export function DeviceGrid({ devices }: { devices: any[] }) {
|
|||
|
||||
return (
|
||||
<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 gap-3 px-6 py-4 bg-muted rounded-lg border-2 border-border">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{Array.from({ length: totalRows }).map((_, i) => renderRow(i))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
@ -45,17 +71,17 @@ export function RequestUpdateMenu({
|
|||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-56">
|
||||
<DropdownMenuItem onClick={onUpdateDevice} disabled={loading}>
|
||||
<DropdownMenuItem onClick={handleUpdateDevice} disabled={loading}>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
<span>Cập nhật thiết bị cụ thể</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={onUpdateRoom} disabled={loading}>
|
||||
<DropdownMenuItem onClick={handleUpdateRoom} disabled={loading}>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
<span>Cập nhật theo phòng</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={onUpdateAll} disabled={loading}>
|
||||
<DropdownMenuItem onClick={handleUpdateAll} disabled={loading}>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
<span>Cập nhật tất cả thiết bị</span>
|
||||
</DropdownMenuItem>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ export const API_ENDPOINTS = {
|
|||
REQUEST_UPDATE_BLACKLIST: `${BASE_URL}/AppVersion/blacklist/request-update`,
|
||||
},
|
||||
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_ROOM_LIST: `${BASE_URL}/DeviceComm/rooms`,
|
||||
GET_DEVICE_FROM_ROOM: (roomName: string) =>
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ function AgentsPage() {
|
|||
? [data]
|
||||
: [];
|
||||
|
||||
// Mutation upload
|
||||
const uploadMutation = useMutationData<FormData>({
|
||||
url: BASE_URL + API_ENDPOINTS.APP_VERSION.UPLOAD,
|
||||
method: "POST",
|
||||
|
|
@ -56,7 +55,10 @@ function AgentsPage() {
|
|||
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!"),
|
||||
onError: (error) => {
|
||||
console.error("Update mutation error:", error);
|
||||
toast.error("Gửi yêu cầu thất bại!");
|
||||
},
|
||||
});
|
||||
|
||||
const handleUpload = async (
|
||||
|
|
|
|||
|
|
@ -55,13 +55,23 @@ function AppsComponent() {
|
|||
const installMutation = useMutationData<{ MsiFileIds: number[] }>({
|
||||
url: "",
|
||||
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) => {
|
||||
console.error("Install error:", error);
|
||||
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
|
||||
const columns: ColumnDef<Version>[] = [
|
||||
{ accessorKey: "version", header: "Phiên bản" },
|
||||
|
|
@ -124,7 +134,34 @@ function AppsComponent() {
|
|||
try {
|
||||
for (const roomName of roomNames) {
|
||||
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 },
|
||||
});
|
||||
}
|
||||
|
|
@ -137,13 +174,15 @@ function AppsComponent() {
|
|||
return (
|
||||
<AppManagerTemplate<Version>
|
||||
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}
|
||||
isLoading={isLoading}
|
||||
columns={columns}
|
||||
onUpload={handleUpload}
|
||||
onUpdate={handleInstall}
|
||||
onDownload={handleDonwload}
|
||||
updateLoading={installMutation.isPending}
|
||||
downloadLoading={downloadMutation.isPending}
|
||||
onTableInit={setTable}
|
||||
rooms={roomData}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { useMutationData } from "@/hooks/useMutationData";
|
|||
import { useQueryData } from "@/hooks/useQueryData";
|
||||
import { BASE_URL, API_ENDPOINTS } from "@/config/api";
|
||||
import { toast } from "sonner";
|
||||
import type { Room } from "@/types/room";
|
||||
|
||||
type SendCommandRequest = { Command: string };
|
||||
type SendCommandResponse = { status: string; message: string };
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ interface AppManagerTemplateProps<TData> {
|
|||
) => Promise<void>;
|
||||
onUpdate?: (targetNames: string[]) => Promise<void> | void;
|
||||
updateLoading?: boolean;
|
||||
onDownload?: (targetNames: string[]) => Promise<void> | void;
|
||||
downloadLoading?: boolean;
|
||||
onTableInit?: (table: any) => void;
|
||||
rooms?: Room[];
|
||||
devices?: string[];
|
||||
|
|
@ -46,12 +48,14 @@ export function AppManagerTemplate<TData>({
|
|||
onUpload,
|
||||
onUpdate,
|
||||
updateLoading,
|
||||
onDownload,
|
||||
downloadLoading,
|
||||
onTableInit,
|
||||
rooms = [],
|
||||
devices = [],
|
||||
}: AppManagerTemplateProps<TData>) {
|
||||
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 = () => {
|
||||
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 () => {
|
||||
if (!onUpdate) return;
|
||||
// Assuming Room has an 'id' property; adjust if needed
|
||||
const roomIds = rooms.map((room) =>
|
||||
typeof room === "string" ? room : room.name
|
||||
);
|
||||
const allTargets = [...roomIds, ...devices];
|
||||
await onUpdate(allTargets);
|
||||
try {
|
||||
const roomIds = rooms.map((room) =>
|
||||
typeof room === "string" ? room : room.name
|
||||
);
|
||||
const allTargets = [...roomIds, ...devices];
|
||||
await onUpdate(allTargets);
|
||||
} catch (e) {
|
||||
console.error("Update error:", e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -113,13 +134,28 @@ export function AppManagerTemplate<TData>({
|
|||
</CardContent>
|
||||
|
||||
{onUpdate && (
|
||||
<CardFooter>
|
||||
<CardFooter className="gap-2">
|
||||
<RequestUpdateMenu
|
||||
onUpdateDevice={openDeviceDialog}
|
||||
onUpdateRoom={openRoomDialog}
|
||||
onUpdateAll={handleUpdateAll}
|
||||
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>
|
||||
)}
|
||||
</Card>
|
||||
|
|
@ -128,15 +164,25 @@ export function AppManagerTemplate<TData>({
|
|||
{dialogType === "room" && (
|
||||
<SelectDialog
|
||||
open={dialogOpen}
|
||||
onClose={() => setDialogOpen(false)}
|
||||
onClose={() => {
|
||||
setDialogOpen(false);
|
||||
setDialogType(null);
|
||||
}}
|
||||
title="Chọn phòng"
|
||||
description="Chọn các phòng cần cập nhật"
|
||||
icon={<Building2 className="w-6 h-6 text-primary" />}
|
||||
items={mapRoomsToSelectItems(rooms)}
|
||||
onConfirm={async (selectedItems) => {
|
||||
if (!onUpdate) return;
|
||||
await onUpdate(selectedItems);
|
||||
setDialogOpen(false);
|
||||
try {
|
||||
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" && (
|
||||
<DeviceSearchDialog
|
||||
open={dialogOpen && dialogType === "device"}
|
||||
onClose={() => setDialogOpen(false)}
|
||||
onClose={() => {
|
||||
setDialogOpen(false);
|
||||
setDialogType(null);
|
||||
}}
|
||||
rooms={rooms}
|
||||
fetchDevices={fetchDevicesFromRoom} // ⬅ thêm vào đây
|
||||
onSelect={(deviceIds) => onUpdate && onUpdate(deviceIds)}
|
||||
fetchDevices={fetchDevicesFromRoom}
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -59,14 +59,18 @@ export function FormSubmitTemplate({
|
|||
};
|
||||
|
||||
// Gửi cho tất cả
|
||||
const handleSubmitAll = () => {
|
||||
const handleSubmitAll = async () => {
|
||||
if (!onSubmit) return;
|
||||
const roomIds = rooms.map((room) =>
|
||||
typeof room === "string" ? room : room.name
|
||||
);
|
||||
const allTargets = [...roomIds, ...devices];
|
||||
for (const target of allTargets) {
|
||||
onSubmit(target, command);
|
||||
try {
|
||||
const roomIds = rooms.map((room) =>
|
||||
typeof room === "string" ? room : room.name
|
||||
);
|
||||
const allTargets = [...roomIds, ...devices];
|
||||
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" && (
|
||||
<SelectDialog
|
||||
open={dialogOpen}
|
||||
onClose={() => setDialogOpen(false)}
|
||||
onClose={() => {
|
||||
setDialogOpen(false);
|
||||
setDialogType(null);
|
||||
}}
|
||||
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={<Building2 className="w-6 h-6 text-primary" />}
|
||||
items={mapRoomsToSelectItems(rooms)}
|
||||
onConfirm={async (selectedItems) => {
|
||||
if (!onSubmit) return;
|
||||
for (const item of selectedItems) {
|
||||
await onSubmit(item, command);
|
||||
try {
|
||||
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" && (
|
||||
<DeviceSearchDialog
|
||||
open={dialogOpen && dialogType === "device"}
|
||||
onClose={() => setDialogOpen(false)}
|
||||
onClose={() => {
|
||||
setDialogOpen(false);
|
||||
setDialogType(null);
|
||||
}}
|
||||
rooms={rooms}
|
||||
fetchDevices={fetchDevicesFromRoom} // ⬅ thêm vào đây
|
||||
onSelect={(deviceIds) =>
|
||||
onSubmit &&
|
||||
deviceIds.forEach((deviceId) => onSubmit(deviceId, command))
|
||||
}
|
||||
fetchDevices={fetchDevicesFromRoom}
|
||||
onSelect={async (deviceIds) => {
|
||||
if (!onSubmit) return;
|
||||
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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user