TTMT.ManageWebGUI/src/routes/_authenticated/command/index.tsx

345 lines
10 KiB
TypeScript
Raw Normal View History

2025-09-24 16:13:57 +07:00
import { createFileRoute } from "@tanstack/react-router";
2025-12-11 14:29:06 +07:00
import { useState } from "react";
import { CommandSubmitTemplate } from "@/template/command-submit-template";
import { CommandRegistryForm, type CommandRegistryFormData } from "@/components/forms/command-registry-form";
2025-09-26 17:56:55 +07:00
import { useQueryData } from "@/hooks/useQueryData";
2025-12-11 14:29:06 +07:00
import { useMutationData } from "@/hooks/useMutationData";
import { API_ENDPOINTS, BASE_URL } from "@/config/api";
import type { ColumnDef } from "@tanstack/react-table";
2025-09-24 16:13:57 +07:00
import { toast } from "sonner";
2025-12-11 14:29:06 +07:00
import { Check, X, Edit2, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import type { ShellCommandData } from "@/components/forms/command-form";
2025-09-26 17:56:55 +07:00
2025-12-11 14:29:06 +07:00
interface CommandRegistry {
id: number;
commandName: string;
description?: string;
commandContent: string;
qoS: 0 | 1 | 2;
isRetained: boolean;
createdAt?: string;
updatedAt?: string;
}
2025-09-24 16:13:57 +07:00
export const Route = createFileRoute("/_authenticated/command/")({
2025-12-11 14:29:06 +07:00
head: () => ({ meta: [{ title: "Gửi lệnh từ xa" }] }),
2025-09-24 16:13:57 +07:00
component: CommandPage,
});
function CommandPage() {
2025-12-11 14:29:06 +07:00
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [selectedCommand, setSelectedCommand] = useState<CommandRegistry | null>(null);
const [table, setTable] = useState<any>();
// Fetch commands
const { data: commands = [], isLoading } = useQueryData({
queryKey: ["commands"],
url: API_ENDPOINTS.COMMAND.GET_COMMANDS,
});
// Fetch rooms
2025-09-26 17:56:55 +07:00
const { data: roomData } = useQueryData({
queryKey: ["rooms"],
2025-12-09 18:59:37 +07:00
url: API_ENDPOINTS.DEVICE_COMM.GET_ROOM_LIST,
2025-09-26 17:56:55 +07:00
});
2025-12-11 14:29:06 +07:00
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<CommandRegistryFormData>({
url: API_ENDPOINTS.COMMAND.ADD_COMMAND,
2025-09-24 16:13:57 +07:00
method: "POST",
2025-12-11 14:29:06 +07:00
invalidate: [["commands"]],
onSuccess: () => toast.success("Thêm lệnh thành công!"),
onError: (error) => {
console.error("Add command error:", error);
toast.error("Thêm lệnh thất bại!");
2025-09-24 16:13:57 +07:00
},
2025-12-11 14:29:06 +07:00
});
// Update command mutation
const updateCommandMutation = useMutationData<CommandRegistryFormData>({
url: "",
method: "POST",
invalidate: [["commands"]],
onSuccess: () => toast.success("Cập nhật lệnh thành công!"),
2025-09-24 16:13:57 +07:00
onError: (error) => {
2025-12-11 14:29:06 +07:00
console.error("Update command error:", error);
toast.error("Cập nhật lệnh thất bại!");
2025-09-24 16:13:57 +07:00
},
});
2025-12-11 14:29:06 +07:00
// Delete command mutation
const deleteCommandMutation = useMutationData<any>({
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<CommandRegistry>[] = [
{
accessorKey: "commandName",
header: "Tên lệnh",
cell: ({ getValue }) => (
<span className="font-semibold break-words">{getValue() as string}</span>
),
},
{
accessorKey: "description",
header: "Mô tả",
cell: ({ getValue }) => (
<span className="text-sm text-muted-foreground break-words whitespace-normal">
{(getValue() as string) || "-"}
</span>
),
},
{
accessorKey: "commandContent",
header: "Nội dung lệnh",
cell: ({ getValue }) => (
<code className="text-xs bg-muted/50 p-1 rounded break-words whitespace-normal block">
{(getValue() as string).substring(0, 100)}...
</code>
),
},
{
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 (
<span className={colors[qosValue as 0 | 1 | 2]}>{qosValue}</span>
);
},
},
{
accessorKey: "isRetained",
header: "Lưu trữ",
cell: ({ getValue }) => {
const retained = getValue() as boolean;
return retained ? (
<div className="flex items-center gap-1">
<Check className="h-4 w-4 text-green-600" />
<span className="text-sm text-green-600"></span>
</div>
) : (
<div className="flex items-center gap-1">
<X className="h-4 w-4 text-gray-400" />
<span className="text-sm text-gray-400">Không</span>
</div>
);
},
},
{
id: "select",
header: () => <div className="text-center text-xs">Chọn đ thực thi</div>,
cell: ({ row }) => (
<input
type="checkbox"
checked={row.getIsSelected?.() ?? false}
onChange={row.getToggleSelectedHandler?.()}
/>
),
enableSorting: false,
enableHiding: false,
},
{
id: "actions",
header: () => <div className="text-center text-xs">Hành đng</div>,
cell: ({ row }) => (
<div className="flex gap-2 justify-center">
<Button
size="sm"
variant="ghost"
onClick={() => {
setSelectedCommand(row.original);
setIsDialogOpen(true);
}}
>
<Edit2 className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => handleDeleteCommand(row.original.id)}
disabled={deleteCommandMutation.isPending}
>
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
</div>
),
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,
2025-09-26 17:56:55 +07:00
});
2025-12-11 14:29:06 +07:00
}
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 (
<CommandSubmitTemplate
title="Gửi lệnh từ xa"
description="Quản lý và thực thi các lệnh trên thiết bị"
data={commandList}
isLoading={isLoading}
columns={columns}
dialogOpen={isDialogOpen}
onDialogOpen={setIsDialogOpen}
dialogTitle={selectedCommand ? "Sửa Lệnh" : "Thêm Lệnh Mới"}
onAddNew={() => {
setSelectedCommand(null);
setIsDialogOpen(true);
2025-09-26 17:56:55 +07:00
}}
2025-12-11 14:29:06 +07:00
onTableInit={setTable}
formContent={
<CommandRegistryForm
onSubmit={handleFormSubmit}
closeDialog={() => setIsDialogOpen(false)}
initialData={selectedCommand || undefined}
title={selectedCommand ? "Sửa Lệnh" : "Thêm Lệnh Mới"}
2025-09-26 17:56:55 +07:00
/>
2025-12-11 14:29:06 +07:00
}
onExecuteSelected={handleExecuteSelected}
onExecuteCustom={handleExecuteCustom}
isExecuting={executeCommandMutation.isPending}
rooms={roomData}
/>
2025-09-24 16:13:57 +07:00
);
}