diff --git a/src/components/forms/command-form.tsx b/src/components/forms/command-form.tsx
index f187809..3a63cea 100644
--- a/src/components/forms/command-form.tsx
+++ b/src/components/forms/command-form.tsx
@@ -3,23 +3,72 @@
import { useForm } from "@tanstack/react-form";
import { z } from "zod";
import { Textarea } from "@/components/ui/textarea";
+import { Label } from "@/components/ui/label";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Info } from "lucide-react";
+import { useState } from "react";
+
+export interface ShellCommandData {
+ command: string;
+ qos: 0 | 1 | 2;
+ isRetained: boolean;
+}
interface ShellCommandFormProps {
command: string;
onCommandChange: (value: string) => void;
+ qos?: 0 | 1 | 2;
+ onQoSChange?: (value: 0 | 1 | 2) => void;
+ isRetained?: boolean;
+ onIsRetainedChange?: (value: boolean) => void;
disabled?: boolean;
}
+const QoSDescriptions = {
+ 0: {
+ name: "At Most Once (Fire and Forget)",
+ description:
+ "Gửi lệnh một lần mà không đảm bảo. Nhanh nhất, tiêu tốn ít tài nguyên.",
+ },
+ 1: {
+ name: "At Least Once",
+ description:
+ "Đảm bảo lệnh sẽ được nhận ít nhất một lần. Cân bằng giữa tốc độ và độ tin cậy.",
+ },
+ 2: {
+ name: "Exactly Once",
+ description:
+ "Đảm bảo lệnh được nhận chính xác một lần. Chậm nhất nhưng đáng tin cậy nhất.",
+ },
+};
+
export function ShellCommandForm({
command,
onCommandChange,
+ qos = 0,
+ onQoSChange,
+ isRetained = false,
+ onIsRetainedChange,
disabled,
}: ShellCommandFormProps) {
+ const [selectedQoS, setSelectedQoS] = useState<0 | 1 | 2>(qos);
+
const form = useForm({
defaultValues: { command },
onSubmit: () => {},
});
+ const handleQoSChange = (value: string) => {
+ const newQoS = Number(value) as 0 | 1 | 2;
+ setSelectedQoS(newQoS);
+ onQoSChange?.(newQoS);
+ };
+
+ const handleRetainedChange = (checked: boolean) => {
+ onIsRetainedChange?.(checked);
+ };
+
return (
(
-
+
+
)}
/>
+
+ {/* QoS Selection */}
+
+
+
+
+ {/* QoS Description */}
+
+
+
+
+ {QoSDescriptions[selectedQoS].name}
+
+ {QoSDescriptions[selectedQoS].description}
+
+
+
+
+ {/* Retained Checkbox */}
+
+
+
+
+
+ Broker MQTT sẽ lưu lệnh này và gửi cho client mới khi kết nối. Hữu ích
+ cho các lệnh cấu hình cần duy trì trạng thái.
+
+
+
);
}
diff --git a/src/components/forms/command-registry-form.tsx b/src/components/forms/command-registry-form.tsx
new file mode 100644
index 0000000..1628dc5
--- /dev/null
+++ b/src/components/forms/command-registry-form.tsx
@@ -0,0 +1,379 @@
+import { useForm } from "@tanstack/react-form";
+import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Label } from "@/components/ui/label";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Info } from "lucide-react";
+import { useState } from "react";
+import { toast } from "sonner";
+import { z } from "zod";
+
+interface CommandRegistryFormProps {
+ onSubmit: (data: CommandRegistryFormData) => Promise
;
+ closeDialog?: () => void;
+ initialData?: Partial;
+ title?: string;
+}
+
+export interface CommandRegistryFormData {
+ commandName: string;
+ description?: string;
+ commandContent: string;
+ qos: 0 | 1 | 2;
+ isRetained: boolean;
+}
+
+// Zod validation schema
+const commandRegistrySchema = z.object({
+ commandName: z
+ .string()
+ .min(1, "Tên lệnh không được để trống")
+ .min(3, "Tên lệnh phải có ít nhất 3 ký tự")
+ .max(100, "Tên lệnh tối đa 100 ký tự")
+ .trim(),
+ description: z.string().max(500, "Mô tả tối đa 500 ký tự").optional(),
+ commandContent: z
+ .string()
+ .min(1, "Nội dung lệnh không được để trống")
+ .min(5, "Nội dung lệnh phải có ít nhất 5 ký tự")
+ .trim(),
+ qos: z.union([z.literal(0), z.literal(1), z.literal(2)]),
+ isRetained: z.boolean(),
+});
+
+const QoSLevels = [
+ {
+ level: 0,
+ name: "At Most Once (Fire and Forget)",
+ description:
+ "Gửi lệnh một lần mà không đảm bảo. Nếu broker hoặc client bị ngắt kết nối, lệnh có thể bị mất. Tốc độ nhanh nhất, tiêu tốn ít tài nguyên.",
+ useCase: "Các lệnh không quan trọng, có thể mất mà không ảnh hưởng",
+ },
+ {
+ level: 1,
+ name: "At Least Once",
+ description:
+ "Đảm bảo lệnh sẽ được nhận ít nhất một lần. Có thể gửi lại nếu chưa nhận được ACK. Lệnh có thể được nhận nhiều lần.",
+ useCase: "Hầu hết các lệnh bình thường cần đảm bảo gửi thành công",
+ },
+ {
+ level: 2,
+ name: "Exactly Once",
+ description:
+ "Đảm bảo lệnh được nhận chính xác một lần. Sử dụng bắt tay 4 chiều để đảm bảo độ tin cậy cao nhất. Tốc độ chậm hơn, tiêu tốn nhiều tài nguyên.",
+ useCase: "Các lệnh quan trọng như xóa dữ liệu, thay đổi cấu hình",
+ },
+];
+
+export function CommandRegistryForm({
+ onSubmit,
+ closeDialog,
+ initialData,
+ title = "Đăng ký Lệnh Mới",
+}: CommandRegistryFormProps) {
+ const [selectedQoS, setSelectedQoS] = useState(
+ initialData?.qos ?? 0
+ );
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const form = useForm({
+ defaultValues: {
+ commandName: initialData?.commandName || "",
+ description: initialData?.description || "",
+ commandContent: initialData?.commandContent || "",
+ qos: (initialData?.qos || 0) as 0 | 1 | 2,
+ isRetained: initialData?.isRetained || false,
+ },
+ onSubmit: async ({ value }) => {
+ try {
+ // Validate using Zod
+ const validatedData = commandRegistrySchema.parse(value);
+ setIsSubmitting(true);
+ await onSubmit(validatedData as CommandRegistryFormData);
+ toast.success("Lưu lệnh thành công!");
+ if (closeDialog) {
+ closeDialog();
+ }
+ } catch (error: any) {
+ if (error.errors?.length > 0) {
+ toast.error(error.errors[0].message);
+ } else {
+ console.error("Submit error:", error);
+ toast.error("Có lỗi xảy ra khi lưu lệnh!");
+ }
+ } finally {
+ setIsSubmitting(false);
+ }
+ },
+ });
+
+ return (
+
+
+
+ {title}
+
+ Tạo và cấu hình lệnh MQTT mới để điều khiển thiết bị
+
+
+
+
+ {(field: any) => (
+
+
+
field.handleChange(e.target.value)}
+ onBlur={field.handleBlur}
+ disabled={isSubmitting}
+ />
+ {field.state.meta.errors?.length > 0 && (
+
+ {String(field.state.meta.errors[0])}
+
+ )}
+
+ Tên định danh duy nhất cho lệnh này
+
+
+ )}
+
+
+ {/* Mô tả */}
+
+ {(field: any) => (
+
+ )}
+
+
+ {/* Nội dung lệnh */}
+
+ {(field: any) => (
+
+ )}
+
+
+ {/* QoS Level */}
+
+ {(field: any) => (
+
+
+
+ {field.state.meta.errors?.length > 0 && (
+
+ {String(field.state.meta.errors[0])}
+
+ )}
+
+ )}
+
+
+ {/* Chú thích QoS */}
+ {selectedQoS !== null && (
+
+
+
+
+
+ {QoSLevels[selectedQoS].name}
+
+
+ {QoSLevels[selectedQoS].description}
+
+
+ Trường hợp sử dụng:{" "}
+ {QoSLevels[selectedQoS].useCase}
+
+
+
+
+ )}
+
+ {/* Bảng so sánh QoS */}
+
+
+
+ Bảng So Sánh Các Mức QoS
+
+
+
+
+
+
+
+ |
+ Tiêu Chí
+ |
+
+ QoS 0
+ |
+
+ QoS 1
+ |
+
+ QoS 2
+ |
+
+
+
+
+ | Đảm bảo gửi |
+ Không |
+ Có |
+ Chính xác |
+
+
+ | Tốc độ |
+ Nhanh nhất |
+ Trung bình |
+ Chậm nhất |
+
+
+ | Tài nguyên |
+ Ít nhất |
+ Trung bình |
+ Nhiều nhất |
+
+
+ | Độ tin cậy |
+ Thấp |
+ Cao |
+ Cao nhất |
+
+
+ | Số lần nhận tối đa |
+ 1 (hoặc 0) |
+ ≥ 1 |
+ 1 |
+
+
+
+
+
+
+
+ {/* IsRetained Checkbox */}
+
+ {(field: any) => (
+
+
+
+
+
+ Broker MQTT sẽ lưu lệnh này và gửi cho client mới khi
+ kết nối. Hữu ích cho các lệnh cấu hình cần duy trì trạng
+ thái.
+
+
+
+ )}
+
+
+ {/* Submit Button */}
+
+
+ {closeDialog && (
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/src/config/api.ts b/src/config/api.ts
index 3cb4c24..5f38b58 100644
--- a/src/config/api.ts
+++ b/src/config/api.ts
@@ -38,6 +38,13 @@ export const API_ENDPOINTS = {
REQUEST_GET_CLIENT_FOLDER_STATUS: (roomName: string) =>
`${BASE_URL}/DeviceComm/clientfolderstatus/${roomName}`,
},
+ COMMAND:
+ {
+ ADD_COMMAND: `${BASE_URL}/Command/add`,
+ GET_COMMANDS: `${BASE_URL}/Command/all`,
+ UPDATE_COMMAND: (commandId: number) => `${BASE_URL}/Command/update/${commandId}`,
+ DELETE_COMMAND: (commandId: number) => `${BASE_URL}/Command/delete/${commandId}`,
+ },
SSE_EVENTS: {
DEVICE_ONLINE: `${BASE_URL}/Sse/events/onlineDevices`,
DEVICE_OFFLINE: `${BASE_URL}/Sse/events/offlineDevices`,
diff --git a/src/routes/_authenticated/command/index.tsx b/src/routes/_authenticated/command/index.tsx
index 62374a9..eedf115 100644
--- a/src/routes/_authenticated/command/index.tsx
+++ b/src/routes/_authenticated/command/index.tsx
@@ -1,67 +1,344 @@
import { createFileRoute } from "@tanstack/react-router";
-import { FormSubmitTemplate } from "@/template/form-submit-template";
-import { ShellCommandForm } from "@/components/forms/command-form";
-import { useMutationData } from "@/hooks/useMutationData";
+import { useState } from "react";
+import { CommandSubmitTemplate } from "@/template/command-submit-template";
+import { CommandRegistryForm, type CommandRegistryFormData } from "@/components/forms/command-registry-form";
import { useQueryData } from "@/hooks/useQueryData";
-import { API_ENDPOINTS } from "@/config/api";
+import { useMutationData } from "@/hooks/useMutationData";
+import { API_ENDPOINTS, BASE_URL } from "@/config/api";
+import type { ColumnDef } from "@tanstack/react-table";
import { toast } from "sonner";
+import { Check, X, Edit2, Trash2 } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import type { ShellCommandData } from "@/components/forms/command-form";
-type SendCommandRequest = { Command: string };
-type SendCommandResponse = { status: string; message: string };
+interface CommandRegistry {
+ id: number;
+ commandName: string;
+ description?: string;
+ commandContent: string;
+ qoS: 0 | 1 | 2;
+ isRetained: boolean;
+ createdAt?: string;
+ updatedAt?: string;
+}
export const Route = createFileRoute("/_authenticated/command/")({
- head: () => ({ meta: [{ title: "Gửi lệnh CMD" }] }),
+ head: () => ({ meta: [{ title: "Gửi lệnh từ xa" }] }),
component: CommandPage,
});
function CommandPage() {
- // Lấy danh sách phòng từ API
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
+ const [selectedCommand, setSelectedCommand] = useState(null);
+ const [table, setTable] = useState();
+
+ // Fetch commands
+ const { data: commands = [], isLoading } = useQueryData({
+ queryKey: ["commands"],
+ url: API_ENDPOINTS.COMMAND.GET_COMMANDS,
+ });
+
+ // Fetch rooms
const { data: roomData } = useQueryData({
queryKey: ["rooms"],
url: API_ENDPOINTS.DEVICE_COMM.GET_ROOM_LIST,
});
- // Mutation gửi lệnh
- const sendCommandMutation = useMutationData<
- SendCommandRequest,
- SendCommandResponse
- >({
- url: "", // sẽ set động theo roomName khi gọi
+ const commandList: CommandRegistry[] = Array.isArray(commands)
+ ? commands.map((cmd: any) => ({
+ ...cmd,
+ qoS: cmd.qoS ?? 0,
+ isRetained: cmd.isRetained ?? false,
+ }))
+ : commands
+ ? [{
+ ...commands,
+ qoS: commands.qoS ?? 0,
+ isRetained: commands.isRetained ?? false,
+ }]
+ : [];
+
+ // Add command mutation
+ const addCommandMutation = useMutationData({
+ url: API_ENDPOINTS.COMMAND.ADD_COMMAND,
method: "POST",
- onSuccess: (data) => {
- if (data.status === "OK") {
- toast.success("Gửi lệnh thành công!");
- } else {
- toast.error("Gửi lệnh thất bại!");
- }
- },
+ invalidate: [["commands"]],
+ onSuccess: () => toast.success("Thêm lệnh thành công!"),
onError: (error) => {
- console.error("Send command error:", error);
- toast.error("Gửi lệnh thất bại!");
+ console.error("Add command error:", error);
+ toast.error("Thêm lệnh thất bại!");
},
});
- return (
- {
- sendCommandMutation.mutateAsync({
- url: API_ENDPOINTS.DEVICE_COMM.SEND_COMMAND(roomName),
- data: { Command: command },
- });
- }}
- submitLoading={sendCommandMutation.isPending}
- >
- {({ command, setCommand }) => (
- ({
+ url: "",
+ method: "POST",
+ invalidate: [["commands"]],
+ onSuccess: () => toast.success("Cập nhật lệnh thành công!"),
+ onError: (error) => {
+ console.error("Update command error:", error);
+ toast.error("Cập nhật lệnh thất bại!");
+ },
+ });
+
+ // Delete command mutation
+ const deleteCommandMutation = useMutationData({
+ url: "",
+ method: "DELETE",
+ invalidate: [["commands"]],
+ onSuccess: () => toast.success("Xóa lệnh thành công!"),
+ onError: (error) => {
+ console.error("Delete command error:", error);
+ toast.error("Xóa lệnh thất bại!");
+ },
+ });
+
+ // Execute command mutation
+ const executeCommandMutation = useMutationData<{
+ commandIds?: number[];
+ command?: ShellCommandData;
+ }>({
+ url: "",
+ method: "POST",
+ onSuccess: () => toast.success("Gửi yêu cầu thực thi lệnh thành công!"),
+ onError: (error) => {
+ console.error("Execute command error:", error);
+ toast.error("Gửi yêu cầu thực thi thất bại!");
+ },
+ });
+
+ // Columns for command table
+ const columns: ColumnDef[] = [
+ {
+ accessorKey: "commandName",
+ header: "Tên lệnh",
+ cell: ({ getValue }) => (
+ {getValue() as string}
+ ),
+ },
+ {
+ accessorKey: "description",
+ header: "Mô tả",
+ cell: ({ getValue }) => (
+
+ {(getValue() as string) || "-"}
+
+ ),
+ },
+ {
+ accessorKey: "commandContent",
+ header: "Nội dung lệnh",
+ cell: ({ getValue }) => (
+
+ {(getValue() as string).substring(0, 100)}...
+
+ ),
+ },
+ {
+ accessorKey: "qoS",
+ header: "QoS",
+ cell: ({ getValue }) => {
+ const qos = getValue() as number | undefined;
+ const qosValue = qos !== undefined ? qos : 0;
+ const colors = {
+ 0: "text-blue-600",
+ 1: "text-amber-600",
+ 2: "text-red-600",
+ };
+ return (
+ {qosValue}
+ );
+ },
+ },
+ {
+ accessorKey: "isRetained",
+ header: "Lưu trữ",
+ cell: ({ getValue }) => {
+ const retained = getValue() as boolean;
+ return retained ? (
+
+
+ Có
+
+ ) : (
+
+
+ Không
+
+ );
+ },
+ },
+ {
+ id: "select",
+ header: () => Chọn để thực thi
,
+ cell: ({ row }) => (
+
- )}
-
+ ),
+ enableSorting: false,
+ enableHiding: false,
+ },
+ {
+ id: "actions",
+ header: () => Hành động
,
+ cell: ({ row }) => (
+
+
+
+
+ ),
+ enableSorting: false,
+ enableHiding: false,
+ },
+ ];
+
+ // Handle form submit
+ const handleFormSubmit = async (data: CommandRegistryFormData) => {
+ if (selectedCommand) {
+ // Update
+ await updateCommandMutation.mutateAsync({
+ url: BASE_URL + API_ENDPOINTS.COMMAND.UPDATE_COMMAND(selectedCommand.id),
+ data,
+ });
+ } else {
+ // Add
+ await addCommandMutation.mutateAsync({
+ data,
+ });
+ }
+ setIsDialogOpen(false);
+ setSelectedCommand(null);
+ };
+
+ // Handle delete
+ const handleDeleteCommand = async (commandId: number) => {
+ if (!confirm("Bạn có chắc muốn xóa lệnh này?")) return;
+
+ try {
+ await deleteCommandMutation.mutateAsync({
+ url: API_ENDPOINTS.COMMAND.DELETE_COMMAND(commandId),
+ data: null,
+ });
+ } catch (error) {
+ console.error("Delete error:", error);
+ }
+ };
+
+ // Handle execute commands from list
+ const handleExecuteSelected = async (targets: 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 lệnh để thực thi!");
+ return;
+ }
+
+ try {
+ for (const target of targets) {
+ for (const row of selectedRows) {
+ // API expects PascalCase directly
+ const apiData = {
+ Command: row.original.commandContent,
+ QoS: row.original.qoS,
+ IsRetained: row.original.isRetained,
+ };
+
+ console.log("[DEBUG] Sending to:", target, "Data:", apiData);
+
+ await executeCommandMutation.mutateAsync({
+ url: API_ENDPOINTS.DEVICE_COMM.SEND_COMMAND(target),
+ data: apiData as any,
+ });
+ }
+ }
+ toast.success("Đã gửi yêu cầu thực thi lệnh cho các mục đã chọn!");
+ if (table) {
+ table.setRowSelection({});
+ }
+ } catch (error) {
+ console.error("[DEBUG] Execute error:", error);
+ console.error("[DEBUG] Response:", (error as any)?.response?.data);
+ toast.error("Có lỗi xảy ra khi thực thi!");
+ }
+ };
+
+ // Handle execute custom command
+ const handleExecuteCustom = async (targets: string[], commandData: ShellCommandData) => {
+ try {
+ for (const target of targets) {
+ // API expects PascalCase directly
+ const apiData = {
+ Command: commandData.command,
+ QoS: commandData.qos,
+ IsRetained: commandData.isRetained,
+ };
+
+ console.log("[DEBUG] Sending custom to:", target, "Data:", apiData);
+
+ await executeCommandMutation.mutateAsync({
+ url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.SEND_COMMAND(target),
+ data: apiData as any,
+ });
+ }
+ toast.success("Đã gửi lệnh tùy chỉnh cho các mục đã chọn!");
+ } catch (error) {
+ console.error("[DEBUG] Execute custom error:", error);
+ console.error("[DEBUG] Response:", (error as any)?.response?.data);
+ toast.error("Gửi lệnh tùy chỉnh thất bại!");
+ }
+ };
+
+ return (
+ {
+ setSelectedCommand(null);
+ setIsDialogOpen(true);
+ }}
+ onTableInit={setTable}
+ formContent={
+ setIsDialogOpen(false)}
+ initialData={selectedCommand || undefined}
+ title={selectedCommand ? "Sửa Lệnh" : "Thêm Lệnh Mới"}
+ />
+ }
+ onExecuteSelected={handleExecuteSelected}
+ onExecuteCustom={handleExecuteCustom}
+ isExecuting={executeCommandMutation.isPending}
+ rooms={roomData}
+ />
);
}
diff --git a/src/template/command-submit-template.tsx b/src/template/command-submit-template.tsx
new file mode 100644
index 0000000..647e57c
--- /dev/null
+++ b/src/template/command-submit-template.tsx
@@ -0,0 +1,424 @@
+import { useState } from "react";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Plus, CommandIcon, Zap, Building2 } from "lucide-react";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import type { ColumnDef } from "@tanstack/react-table";
+import { VersionTable } from "@/components/tables/version-table";
+import {
+ ShellCommandForm,
+ type ShellCommandData,
+} from "@/components/forms/command-form";
+import { RequestUpdateMenu } from "@/components/menu/request-update-menu";
+import { SelectDialog } from "@/components/dialogs/select-dialog";
+import { DeviceSearchDialog } from "@/components/bars/device-searchbar";
+import { mapRoomsToSelectItems } from "@/helpers/mapRoomToSelectItems";
+import { fetchDevicesFromRoom } from "@/services/device.service";
+import type { Room } from "@/types/room";
+import { toast } from "sonner";
+
+interface CommandSubmitTemplateProps {
+ title: string;
+ description: string;
+
+ // Data & Loading
+ data: T[];
+ isLoading?: boolean;
+
+ // Table config
+ columns: ColumnDef[];
+
+ // Dialog
+ dialogOpen: boolean;
+ onDialogOpen: (open: boolean) => void;
+ dialogTitle?: string;
+ formContent?: React.ReactNode;
+ dialogContentClassName?: string;
+
+ // Actions
+ onAddNew?: () => void;
+ onTableInit?: (table: any) => void;
+
+ // Execute
+ onExecuteSelected?: (targets: string[]) => void;
+ onExecuteCustom?: (targets: string[], commandData: ShellCommandData) => void;
+ isExecuting?: boolean;
+
+ // Execution scope
+ rooms?: Room[];
+ devices?: string[];
+}
+
+export function CommandSubmitTemplate({
+ title,
+ description,
+ data,
+ isLoading = false,
+ columns,
+ dialogOpen,
+ onDialogOpen,
+ dialogTitle = "Thêm Mục Mới",
+ formContent,
+ dialogContentClassName,
+ onAddNew,
+ onTableInit,
+ onExecuteSelected,
+ onExecuteCustom,
+ isExecuting = false,
+ rooms = [],
+ devices = [],
+}: CommandSubmitTemplateProps) {
+ const [activeTab, setActiveTab] = useState<"list" | "execute">("list");
+ const [customCommand, setCustomCommand] = useState("");
+ const [customQoS, setCustomQoS] = useState<0 | 1 | 2>(0);
+ const [customRetained, setCustomRetained] = useState(false);
+ const [table, setTable] = useState();
+ const [dialogOpen2, setDialogOpen2] = useState(false);
+ const [dialogType, setDialogType] = useState<"room" | "device" | "room-custom" | "device-custom" | null>(null);
+
+ const handleTableInit = (t: any) => {
+ setTable(t);
+ onTableInit?.(t);
+ };
+
+ const openRoomDialog = () => {
+ if (rooms.length > 0 && onExecuteSelected) {
+ setDialogType("room");
+ setDialogOpen2(true);
+ }
+ };
+
+ const openDeviceDialog = () => {
+ if (onExecuteSelected) {
+ setDialogType("device");
+ setDialogOpen2(true);
+ }
+ };
+
+ const handleExecuteSelected = () => {
+ 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 mục để thực thi!");
+ return;
+ }
+
+ onExecuteSelected?.([]);
+ };
+
+ const handleExecuteAll = () => {
+ if (!onExecuteSelected) return;
+ try {
+ const roomNames = rooms.map((room) =>
+ typeof room === "string" ? room : room.name
+ );
+ const allTargets = [...roomNames, ...devices];
+ onExecuteSelected(allTargets);
+ } catch (e) {
+ console.error("Execute error:", e);
+ }
+ };
+
+ const handleExecuteCustom = async (targets: string[]) => {
+ if (!customCommand.trim()) {
+ toast.error("Vui lòng nhập lệnh!");
+ return;
+ }
+
+ const shellCommandData: ShellCommandData = {
+ command: customCommand,
+ qos: customQoS,
+ isRetained: customRetained,
+ };
+
+ try {
+ await onExecuteCustom?.(targets, shellCommandData);
+ setCustomCommand("");
+ setCustomQoS(0);
+ setCustomRetained(false);
+ } catch (e) {
+ console.error("Execute custom command error:", e);
+ }
+ };
+
+ const handleExecuteCustomAll = () => {
+ if (!onExecuteCustom) return;
+ try {
+ const roomNames = rooms.map((room) =>
+ typeof room === "string" ? room : room.name
+ );
+ const allTargets = [...roomNames, ...devices];
+ handleExecuteCustom(allTargets);
+ } catch (e) {
+ console.error("Execute error:", e);
+ }
+ };
+
+ const openRoomDialogCustom = () => {
+ if (rooms.length > 0 && onExecuteCustom) {
+ setDialogType("room-custom");
+ setDialogOpen2(true);
+ }
+ };
+
+ const openDeviceDialogCustom = () => {
+ if (onExecuteCustom) {
+ setDialogType("device-custom");
+ setDialogOpen2(true);
+ }
+ };
+
+ return (
+
+
+
{title}
+
{description}
+
+
+
+
+
+
+ {title}
+
+
+ {onAddNew && (
+
+ )}
+
+
+
+ {/* Tabs Navigation */}
+
+
+
+
+
+ {/* Tab 1: Danh sách */}
+ {activeTab === "list" && (
+
+
+ data={data}
+ columns={columns}
+ isLoading={isLoading}
+ onTableInit={handleTableInit}
+ />
+ }
+ />
+
+ )}
+
+ {/* Tab 2: Thực thi */}
+ {activeTab === "execute" && (
+
+ {/* Lệnh tùy chỉnh */}
+
+
+
+ Thực Thi Lệnh Tùy Chỉnh
+
+
+ Nhập lệnh tuỳ chỉnh với QoS và Retained settings
+
+
+
+
+
+ }
+ />
+
+
+
+
+ )}
+
+
+ {/* Dialog chọn phòng - Thực thi */}
+ {dialogType === "room" && (
+
{
+ setDialogOpen2(false);
+ setDialogType(null);
+ setTimeout(() => window.location.reload(), 500);
+ }}
+ title="Chọn phòng"
+ description="Chọn các phòng để thực thi lệnh"
+ icon={}
+ items={mapRoomsToSelectItems(rooms)}
+ onConfirm={async (selectedItems) => {
+ if (!onExecuteSelected) return;
+ try {
+ await onExecuteSelected(selectedItems);
+ } catch (e) {
+ console.error("Execute error:", e);
+ } finally {
+ setDialogOpen2(false);
+ setDialogType(null);
+ setTimeout(() => window.location.reload(), 500);
+ }
+ }}
+ />
+ )}
+
+ {/* Dialog tìm thiết bị - Thực thi */}
+ {dialogType === "device" && (
+ {
+ setDialogOpen2(false);
+ setDialogType(null);
+ setTimeout(() => window.location.reload(), 500);
+ }}
+ rooms={rooms}
+ fetchDevices={fetchDevicesFromRoom}
+ onSelect={async (deviceIds) => {
+ if (!onExecuteSelected) {
+ setDialogOpen2(false);
+ setDialogType(null);
+ setTimeout(() => window.location.reload(), 500);
+ return;
+ }
+ try {
+ await onExecuteSelected(deviceIds);
+ } catch (e) {
+ console.error("Execute error:", e);
+ } finally {
+ setDialogOpen2(false);
+ setDialogType(null);
+ setTimeout(() => window.location.reload(), 500);
+ }
+ }}
+ />
+ )}
+
+ {/* Dialog chọn phòng - Thực thi lệnh tùy chỉnh */}
+ {dialogType === "room-custom" && (
+ {
+ setDialogOpen2(false);
+ setDialogType(null);
+ setTimeout(() => window.location.reload(), 500);
+ }}
+ title="Chọn phòng"
+ description="Chọn các phòng để thực thi lệnh tùy chỉnh"
+ icon={}
+ items={mapRoomsToSelectItems(rooms)}
+ onConfirm={async (selectedItems) => {
+ try {
+ await handleExecuteCustom(selectedItems);
+ } catch (e) {
+ console.error("Execute error:", e);
+ } finally {
+ setDialogOpen2(false);
+ setDialogType(null);
+ setTimeout(() => window.location.reload(), 500);
+ }
+ }}
+ />
+ )}
+
+ {/* Dialog tìm thiết bị - Thực thi lệnh tùy chỉnh */}
+ {dialogType === "device-custom" && (
+ {
+ setDialogOpen2(false);
+ setDialogType(null);
+ setTimeout(() => window.location.reload(), 500);
+ }}
+ rooms={rooms}
+ fetchDevices={fetchDevicesFromRoom}
+ onSelect={async (deviceIds) => {
+ try {
+ await handleExecuteCustom(deviceIds);
+ } catch (e) {
+ console.error("Execute error:", e);
+ } finally {
+ setDialogOpen2(false);
+ setDialogType(null);
+ setTimeout(() => window.location.reload(), 500);
+ }
+ }}
+ />
+ )}
+
+ {/* Dialog for add/edit */}
+ {formContent && (
+
+ )}
+
+ );
+}