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

263 lines
8.1 KiB
TypeScript
Raw Normal View History

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";
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);
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",
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!");
setUploadPercent(0);
2025-08-11 23:21:36 +07:00
form.reset();
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-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 Agent</h1>
<p className="text-muted-foreground mt-2">
Quản 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 nhập số phiên bản
</DialogDescription>
</DialogHeader>
<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"
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>
{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>
<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 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>
);
}