2025-08-11 23:21:36 +07:00
|
|
|
import { createFileRoute } from "@tanstack/react-router";
|
|
|
|
import {
|
|
|
|
Card,
|
|
|
|
CardContent,
|
|
|
|
CardDescription,
|
|
|
|
CardFooter,
|
|
|
|
CardHeader,
|
|
|
|
CardTitle,
|
|
|
|
} from "@/components/ui/card";
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
import { useQueryData } from "@/hooks/useQueryData";
|
|
|
|
import { useMutationData } from "@/hooks/useMutationData";
|
|
|
|
import { formOptions, useForm } from "@tanstack/react-form";
|
|
|
|
import { toast } from "sonner";
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
import { useState } from "react";
|
|
|
|
import {
|
|
|
|
Dialog,
|
|
|
|
DialogContent,
|
|
|
|
DialogDescription,
|
|
|
|
DialogFooter,
|
|
|
|
DialogHeader,
|
|
|
|
DialogTitle,
|
|
|
|
DialogTrigger,
|
|
|
|
} from "@/components/ui/dialog";
|
|
|
|
import { FileText, Plus } from "lucide-react";
|
|
|
|
import {
|
|
|
|
Table,
|
|
|
|
TableBody,
|
|
|
|
TableCell,
|
|
|
|
TableHead,
|
|
|
|
TableHeader,
|
|
|
|
TableRow,
|
|
|
|
} from "@/components/ui/table";
|
2025-08-27 22:36:27 +07:00
|
|
|
import { Progress } from "@/components/ui/progress";
|
2025-08-11 23:21:36 +07:00
|
|
|
|
|
|
|
import { BASE_URL, API_ENDPOINTS } from "@/config/api";
|
|
|
|
|
|
|
|
interface UploadAppFormProps {
|
|
|
|
files: FileList;
|
|
|
|
newVersion: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
const defaultInput: UploadAppFormProps = {
|
|
|
|
files: new DataTransfer().files,
|
|
|
|
newVersion: "",
|
|
|
|
};
|
|
|
|
|
|
|
|
const formOpts = formOptions({
|
|
|
|
defaultValues: defaultInput,
|
|
|
|
});
|
|
|
|
|
|
|
|
export const Route = createFileRoute("/_authenticated/apps/")({
|
|
|
|
head: () => ({
|
|
|
|
meta: [{ title: "Quản lý Agent" }],
|
|
|
|
}),
|
|
|
|
component: AppsComponent,
|
|
|
|
});
|
|
|
|
|
|
|
|
function AppsComponent() {
|
|
|
|
const { data: versionData, isLoading } = useQueryData({
|
|
|
|
queryKey: ["app-version"],
|
|
|
|
url: BASE_URL + API_ENDPOINTS.APP_VERSION.GET_VERSION,
|
|
|
|
});
|
|
|
|
|
|
|
|
const versionList = Array.isArray(versionData)
|
|
|
|
? versionData
|
|
|
|
: versionData
|
|
|
|
? [versionData]
|
|
|
|
: [];
|
|
|
|
|
|
|
|
const [isUploadOpen, setIsUploadOpen] = useState(false);
|
2025-08-27 22:36:27 +07:00
|
|
|
const [uploadPercent, setUploadPercent] = useState(0);
|
2025-08-11 23:21:36 +07:00
|
|
|
|
|
|
|
const uploadMutation = useMutationData<FormData>({
|
|
|
|
url: BASE_URL + API_ENDPOINTS.APP_VERSION.UPLOAD,
|
|
|
|
method: "POST",
|
2025-08-27 22:36:27 +07:00
|
|
|
config: {
|
|
|
|
onUploadProgress: (e: ProgressEvent) => {
|
|
|
|
if (e.total) {
|
|
|
|
const percent = Math.round((e.loaded * 100) / e.total);
|
|
|
|
setUploadPercent(percent);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
2025-08-11 23:21:36 +07:00
|
|
|
onSuccess: () => {
|
|
|
|
toast.success("Cập nhật thành công!");
|
2025-08-27 22:36:27 +07:00
|
|
|
setUploadPercent(0);
|
2025-08-11 23:21:36 +07:00
|
|
|
form.reset();
|
2025-08-27 22:36:27 +07:00
|
|
|
setIsUploadOpen(false);
|
2025-08-11 23:21:36 +07:00
|
|
|
},
|
|
|
|
onError: () => toast.error("Lỗi khi cập nhật phiên bản!"),
|
|
|
|
});
|
|
|
|
|
|
|
|
const updateAgentMutation = useMutationData<void>({
|
|
|
|
url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.UPDATE_AGENT,
|
|
|
|
method: "POST",
|
|
|
|
onSuccess: () => toast.success("Đã gửi yêu cầu cập nhật đến thiết bị!"),
|
|
|
|
onError: () => toast.error("Gửi yêu cầu thất bại!"),
|
|
|
|
});
|
|
|
|
|
|
|
|
const form = useForm({
|
|
|
|
...formOpts,
|
|
|
|
onSubmit: async ({ value }) => {
|
|
|
|
const typedValue = value as UploadAppFormProps;
|
|
|
|
if (!typedValue.newVersion || typedValue.files.length === 0) {
|
|
|
|
toast.error("Vui lòng điền đầy đủ thông tin");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const formData = new FormData();
|
|
|
|
Array.from(typedValue.files).forEach((file) =>
|
|
|
|
formData.append("files", file)
|
|
|
|
);
|
|
|
|
formData.append("newVersion", typedValue.newVersion);
|
2025-08-27 22:36:27 +07:00
|
|
|
|
2025-08-11 23:21:36 +07:00
|
|
|
await uploadMutation.mutateAsync(formData);
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="w-full px-6 space-y-4">
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
<div>
|
|
|
|
<h1 className="text-3xl font-bold">Quản lý Agent</h1>
|
|
|
|
<p className="text-muted-foreground mt-2">
|
|
|
|
Quản lý và theo dõi các phiên bản Agent
|
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<Dialog open={isUploadOpen} onOpenChange={setIsUploadOpen}>
|
|
|
|
<DialogTrigger asChild>
|
|
|
|
<Button className="gap-2">
|
|
|
|
<Plus className="h-4 w-4" />
|
|
|
|
Cập nhật phiên bản mới
|
|
|
|
</Button>
|
|
|
|
</DialogTrigger>
|
|
|
|
<DialogContent className="sm:max-w-md">
|
|
|
|
<DialogHeader>
|
|
|
|
<DialogTitle>Cập nhật phiên bản mới</DialogTitle>
|
|
|
|
<DialogDescription>
|
|
|
|
Chọn tệp và nhập số phiên bản
|
|
|
|
</DialogDescription>
|
|
|
|
</DialogHeader>
|
|
|
|
|
2025-08-27 22:36:27 +07:00
|
|
|
<form className="space-y-4" onSubmit={form.handleSubmit}>
|
2025-08-11 23:21:36 +07:00
|
|
|
<form.Field name="newVersion">
|
|
|
|
{(field) => (
|
|
|
|
<div className="space-y-2">
|
|
|
|
<Label>Phiên bản</Label>
|
|
|
|
<Input
|
|
|
|
value={field.state.value}
|
|
|
|
onChange={(e) => field.handleChange(e.target.value)}
|
|
|
|
placeholder="e.g., 1.0.0"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</form.Field>
|
|
|
|
|
|
|
|
<form.Field name="files">
|
|
|
|
{(field) => (
|
|
|
|
<div className="space-y-2">
|
|
|
|
<Label>File ứng dụng</Label>
|
|
|
|
<Input
|
|
|
|
type="file"
|
2025-08-27 22:36:27 +07:00
|
|
|
accept=".exe,.msi,.apk"
|
2025-08-11 23:21:36 +07:00
|
|
|
onChange={(e) => {
|
|
|
|
if (e.target.files) {
|
|
|
|
field.handleChange(e.target.files);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</form.Field>
|
|
|
|
|
2025-08-27 22:36:27 +07:00
|
|
|
{uploadPercent > 0 && (
|
|
|
|
<div className="space-y-2">
|
|
|
|
<Label>Tiến trình upload</Label>
|
|
|
|
<Progress value={uploadPercent} className="w-full" />
|
|
|
|
<span>{uploadPercent}%</span>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
2025-08-11 23:21:36 +07:00
|
|
|
<DialogFooter>
|
|
|
|
<Button
|
|
|
|
type="button"
|
|
|
|
variant="outline"
|
|
|
|
onClick={() => setIsUploadOpen(false)}
|
|
|
|
>
|
|
|
|
Hủy
|
|
|
|
</Button>
|
2025-08-27 22:36:27 +07:00
|
|
|
<Button type="submit" disabled={uploadMutation.isPending}>
|
|
|
|
{uploadMutation.isPending ? "Đang tải..." : "Tải lên"}
|
|
|
|
</Button>
|
2025-08-11 23:21:36 +07:00
|
|
|
</DialogFooter>
|
|
|
|
</form>
|
|
|
|
</DialogContent>
|
|
|
|
</Dialog>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<Card className="w-full">
|
|
|
|
<CardHeader>
|
|
|
|
<CardTitle className="flex items-center gap-2">
|
|
|
|
<FileText className="h-5 w-5" />
|
|
|
|
Lịch sử phiên bản
|
|
|
|
</CardTitle>
|
|
|
|
<CardDescription>
|
|
|
|
Tất cả các phiên bản đã tải lên của Agent
|
|
|
|
</CardDescription>
|
|
|
|
</CardHeader>
|
|
|
|
<CardContent>
|
|
|
|
<Table>
|
|
|
|
<TableHeader>
|
|
|
|
<TableRow>
|
|
|
|
<TableHead>Phiên bản</TableHead>
|
|
|
|
<TableHead>Tên tệp</TableHead>
|
|
|
|
<TableHead>Đường dẫn thư mục</TableHead>
|
|
|
|
<TableHead>Thời gian cập nhật</TableHead>
|
|
|
|
</TableRow>
|
|
|
|
</TableHeader>
|
|
|
|
<TableBody>
|
|
|
|
{isLoading ? (
|
|
|
|
<TableRow>
|
|
|
|
<TableCell colSpan={4}>Đang tải dữ liệu...</TableCell>
|
|
|
|
</TableRow>
|
|
|
|
) : versionList.length === 0 ? (
|
|
|
|
<TableRow>
|
|
|
|
<TableCell colSpan={4}>Không có dữ liệu phiên bản.</TableCell>
|
|
|
|
</TableRow>
|
|
|
|
) : (
|
|
|
|
versionList.map((v: any) => (
|
|
|
|
<TableRow key={v.id || v.version}>
|
|
|
|
<TableCell>{v.version}</TableCell>
|
|
|
|
<TableCell>{v.fileName}</TableCell>
|
|
|
|
<TableCell>{v.folderPath}</TableCell>
|
|
|
|
<TableCell>
|
|
|
|
{v.updatedAt
|
|
|
|
? new Date(v.updatedAt).toLocaleString("vi-VN")
|
|
|
|
: "N/A"}
|
|
|
|
</TableCell>
|
|
|
|
</TableRow>
|
|
|
|
))
|
|
|
|
)}
|
|
|
|
</TableBody>
|
|
|
|
</Table>
|
|
|
|
</CardContent>
|
|
|
|
<CardFooter>
|
|
|
|
<Button
|
|
|
|
variant="outline"
|
|
|
|
onClick={() => updateAgentMutation.mutateAsync()}
|
|
|
|
disabled={updateAgentMutation.isPending}
|
|
|
|
>
|
|
|
|
{updateAgentMutation.isPending
|
|
|
|
? "Đang gửi..."
|
|
|
|
: "Yêu cầu thiết bị cập nhật"}
|
|
|
|
</Button>
|
|
|
|
</CardFooter>
|
|
|
|
</Card>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|