TTMT.ManageWebGUI/src/components/bars/device-action-bar.tsx

227 lines
7.4 KiB
TypeScript
Raw Normal View History

2026-05-30 22:16:02 +07:00
import { useMemo, useState } from "react";
import { AlertTriangle, CheckCircle2, Power, PowerOff, RotateCcw, ShieldBan, XCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useMachineNumber } from "@/hooks/useMachineNumber";
import { useExecuteSensitiveCommand, useGetSensitiveCommands } from "@/hooks/queries/useCommandQueries";
import { CommandType } from "@/types/command-registry";
import { toast } from "sonner";
interface DeviceActionBarProps {
roomName: string;
selectedDevices: any[];
onClearSelection: () => void;
}
const ACTIONS = [
{
type: CommandType.RESTART,
label: "Khởi động lại",
icon: Power,
variant: "outline" as const,
},
{
type: CommandType.SHUTDOWN,
label: "Tắt máy",
icon: PowerOff,
variant: "destructive" as const,
},
{
type: CommandType.TASKKILL,
label: "Kết thúc tác vụ",
icon: XCircle,
variant: "outline" as const,
},
{
type: CommandType.BLOCK,
label: "Chặn",
icon: ShieldBan,
variant: "outline" as const,
},
{
type: CommandType.RESET,
label: "Reset",
icon: RotateCcw,
variant: "destructive" as const,
},
];
const DANGER_TYPES = new Set<CommandType>([CommandType.SHUTDOWN, CommandType.RESET]);
export function DeviceActionBar({
roomName,
selectedDevices,
onClearSelection,
}: DeviceActionBarProps) {
const [confirmOpen, setConfirmOpen] = useState(false);
const [activeType, setActiveType] = useState<CommandType | null>(null);
const [password, setPassword] = useState("");
const [isExecuting, setIsExecuting] = useState(false);
const getMachineNumber = useMachineNumber();
const { data: sensitiveCommands = [] } = useGetSensitiveCommands();
const executeSensitiveMutation = useExecuteSensitiveCommand();
const commandsByType = useMemo(() => {
return (Object.values(CommandType) as Array<number | string>)
.filter((value) => typeof value === "number")
.reduce((acc: Record<number, any[]>, type) => {
acc[type as number] = (sensitiveCommands || []).filter(
(command: any) => Number(command.command) === Number(type)
);
return acc;
}, {} as Record<number, any[]>);
}, [sensitiveCommands]);
const selectedCount = selectedDevices.length;
const activeCommand = activeType ? commandsByType[activeType]?.[0] : null;
const buildDeviceLabel = (device: any) => {
const number = getMachineNumber(device?.id || "");
const ipAddress = device?.networkInfos?.[0]?.ipAddress;
if (number > 0) {
return `#${number}${ipAddress ? ` (${ipAddress})` : ""}`;
}
return `${device?.id ?? ""}${ipAddress ? ` (${ipAddress})` : ""}`;
};
const openConfirm = (type: CommandType) => {
if (!commandsByType[type]?.length) {
toast.error("Chưa có lệnh phù hợp cho thao tác này.");
return;
}
setActiveType(type);
setConfirmOpen(true);
};
const handleClose = () => {
if (isExecuting) return;
setConfirmOpen(false);
setActiveType(null);
setPassword("");
};
const handleConfirm = async () => {
if (!activeCommand || !activeType) return;
if (!password.trim()) {
toast.error("Vui lòng nhập mật khẩu xác nhận.");
return;
}
setIsExecuting(true);
try {
await executeSensitiveMutation.mutateAsync({
roomName,
command: activeCommand.commandName,
password,
});
toast.success(`Đã gửi lệnh: ${activeCommand.commandName}`);
handleClose();
onClearSelection();
} catch (error) {
console.error("Execute command error:", error);
toast.error("Lỗi khi gửi lệnh!");
} finally {
setIsExecuting(false);
}
};
if (selectedCount === 0) return null;
return (
<>
<div className="sticky bottom-4 z-30">
<div className="flex flex-col gap-3 rounded-xl border bg-background/95 px-4 py-3 shadow-lg backdrop-blur sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-2 text-sm font-semibold">
<CheckCircle2 className="h-4 w-4 text-emerald-600" />
Đã chọn {selectedCount} thiết bị
</div>
<div className="flex flex-wrap items-center gap-2">
{ACTIONS.map((action) => {
const Icon = action.icon;
const isDisabled = !commandsByType[action.type]?.length;
return (
<Button
key={action.type}
variant={action.variant}
size="sm"
disabled={isDisabled}
onClick={() => openConfirm(action.type)}
>
<Icon className="h-4 w-4" />
{action.label}
</Button>
);
})}
<Button variant="ghost" size="sm" onClick={onClearSelection}>
Bỏ chọn
</Button>
</div>
</div>
</div>
<Dialog open={confirmOpen} onOpenChange={handleClose}>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-orange-600" />
Xác nhận thực thi lệnh
</DialogTitle>
<DialogDescription className="text-left space-y-3">
<p>
Bạn chắc chắn muốn thực thi lệnh{" "}
<strong>{activeCommand?.commandName ?? ""}</strong>?
</p>
{DANGER_TYPES.has(activeType ?? CommandType.RESTART) && (
<p className="text-sm text-destructive">
Hành đng này không thể hoàn tác.
</p>
)}
</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<div className="text-sm font-medium">Thiết bị đưc chọn</div>
<ScrollArea className="max-h-40 rounded-lg border p-2">
<div className="space-y-1 text-sm">
{selectedDevices.map((device) => (
<div key={device.id} className="text-muted-foreground">
{buildDeviceLabel(device)}
</div>
))}
</div>
</ScrollArea>
<div>
<label className="mb-1 block text-sm font-medium">Mật khẩu</label>
<Input
type="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
placeholder="Nhập mật khẩu để xác nhận"
/>
</div>
</div>
<DialogFooter className="flex gap-2 sm:gap-3">
<Button variant="outline" onClick={handleClose} disabled={isExecuting}>
Hủy
</Button>
<Button
variant={DANGER_TYPES.has(activeType ?? CommandType.RESTART) ? "destructive" : "default"}
onClick={handleConfirm}
disabled={isExecuting}
>
{isExecuting ? "Đang gửi..." : "Xác nhận"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}