From 451eed4c65d38b689e21adcfe9c717d2b91a68e0 Mon Sep 17 00:00:00 2001 From: phuongdm Date: Tue, 2 Dec 2025 11:03:15 +0700 Subject: [PATCH] fix UI freeze problem --- src/components/buttons/delete-button.tsx | 81 +++++++++++++++++++++ src/components/forms/upload-file-form.tsx | 20 +++++ src/components/menu/request-update-menu.tsx | 20 ++++- src/components/tables/version-table.tsx | 2 +- src/config/api.ts | 1 + src/routes/_authenticated/apps/index.tsx | 37 ++++++++++ src/template/app-manager-template.tsx | 69 ++++++++++++------ 7 files changed, 204 insertions(+), 26 deletions(-) create mode 100644 src/components/buttons/delete-button.tsx diff --git a/src/components/buttons/delete-button.tsx b/src/components/buttons/delete-button.tsx new file mode 100644 index 0000000..402f0cc --- /dev/null +++ b/src/components/buttons/delete-button.tsx @@ -0,0 +1,81 @@ +import { Button } from "@/components/ui/button"; +import { Trash2 } from "lucide-react"; +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; + +interface DeleteButtonProps { + onClick: () => void | Promise; + loading?: boolean; + disabled?: boolean; + label?: string; + title?: string; + description?: string; +} + +export function DeleteButton({ + onClick, + loading = false, + disabled = false, + label = "Xóa", + title = "Xác nhận xóa", + description = "Bạn có chắc chắn muốn xóa các mục này không?", +}: DeleteButtonProps) { + const [open, setOpen] = useState(false); + const [isConfirming, setIsConfirming] = useState(false); + + const handleConfirm = async () => { + setIsConfirming(true); + try { + await onClick(); + } finally { + setIsConfirming(false); + setOpen(false); + } + }; + + return ( + <> + + + + + + {title} + {description} + + + + + + + + + ); +} diff --git a/src/components/forms/upload-file-form.tsx b/src/components/forms/upload-file-form.tsx index c65a6fe..792be73 100644 --- a/src/components/forms/upload-file-form.tsx +++ b/src/components/forms/upload-file-form.tsx @@ -17,6 +17,13 @@ export function UploadVersionForm({ onSubmit, closeDialog }: UploadVersionFormPr const [isUploading, setIsUploading] = useState(false); const [isDone, setIsDone] = useState(false); + // Match server allowed extensions + const ALLOWED_EXTENSIONS = [".exe", ".apk", ".conf", ".json", ".xml", ".setting", ".lnk", ".url", ".seb"]; + const isFileValid = (file: File) => { + const fileName = file.name.toLowerCase(); + return ALLOWED_EXTENSIONS.some((ext) => fileName.endsWith(ext)); + }; + const form = useForm({ defaultValues: { files: new DataTransfer().files, newVersion: "" }, onSubmit: async ({ value }) => { @@ -25,6 +32,15 @@ export function UploadVersionForm({ onSubmit, closeDialog }: UploadVersionFormPr return; } + // Validate file types + const invalidFiles = Array.from(value.files).filter((f) => !isFileValid(f)); + if (invalidFiles.length > 0) { + toast.error( + `File không hợp lệ: ${invalidFiles.map((f) => f.name).join(", ")}. Chỉ chấp nhận ${ALLOWED_EXTENSIONS.join(", ")}` + ); + return; + } + try { setIsUploading(true); setUploadPercent(0); @@ -81,9 +97,13 @@ export function UploadVersionForm({ onSubmit, closeDialog }: UploadVersionFormPr e.target.files && field.handleChange(e.target.files)} disabled={isUploading || isDone} /> +

+ Chỉ chấp nhận file: .exe, .apk, .conf, .json, .xml, .setting, .lnk, .url, .seb +

)} diff --git a/src/components/menu/request-update-menu.tsx b/src/components/menu/request-update-menu.tsx index d356b4d..eb64275 100644 --- a/src/components/menu/request-update-menu.tsx +++ b/src/components/menu/request-update-menu.tsx @@ -14,6 +14,11 @@ interface RequestUpdateMenuProps { onUpdateRoom: () => void; onUpdateAll: () => void; loading?: boolean; + label?: string; + deviceLabel?: string; + roomLabel?: string; + allLabel?: string; + icon?: React.ReactNode; } export function RequestUpdateMenu({ @@ -21,6 +26,11 @@ export function RequestUpdateMenu({ onUpdateRoom, onUpdateAll, loading, + label = "Cập nhật", + deviceLabel = "Thiết bị cụ thể", + roomLabel = "Theo phòng", + allLabel = "Tất cả thiết bị", + icon, }: RequestUpdateMenuProps) { const [open, setOpen] = useState(false); @@ -58,11 +68,13 @@ export function RequestUpdateMenu({
{loading ? ( + ) : icon ? ( +
{icon}
) : ( )} - {loading ? "Đang gửi..." : "Cập nhật"} + {loading ? "Đang gửi..." : label}
@@ -73,17 +85,17 @@ export function RequestUpdateMenu({ - Cập nhật thiết bị cụ thể + {deviceLabel} - Cập nhật theo phòng + {roomLabel} - Cập nhật tất cả thiết bị + {allLabel} diff --git a/src/components/tables/version-table.tsx b/src/components/tables/version-table.tsx index c1f2525..261e8b0 100644 --- a/src/components/tables/version-table.tsx +++ b/src/components/tables/version-table.tsx @@ -18,7 +18,7 @@ interface VersionTableProps { data: TData[]; columns: ColumnDef[]; isLoading: boolean; - onTableInit?: (table: any) => void; // <-- thêm + onTableInit?: (table: any) => void; } export function VersionTable({ diff --git a/src/config/api.ts b/src/config/api.ts index 91d3bfc..1309725 100644 --- a/src/config/api.ts +++ b/src/config/api.ts @@ -13,6 +13,7 @@ export const API_ENDPOINTS = { ADD_BLACKLIST: `${BASE_URL}/AppVersion/blacklist/add`, DELETE_BLACKLIST: (appId: number) => `${BASE_URL}/AppVersion/blacklist/remove/${appId}`, UPDATE_BLACKLIST: (appId: string) => `${BASE_URL}/AppVersion/blacklist/update/${appId}`, + DELETE_FILES: `${BASE_URL}/AppVersion/delete`, REQUEST_UPDATE_BLACKLIST: `${BASE_URL}/AppVersion/blacklist/request-update`, }, DEVICE_COMM: { diff --git a/src/routes/_authenticated/apps/index.tsx b/src/routes/_authenticated/apps/index.tsx index c36b9d1..f62c630 100644 --- a/src/routes/_authenticated/apps/index.tsx +++ b/src/routes/_authenticated/apps/index.tsx @@ -72,6 +72,17 @@ function AppsComponent() { }, }); + const deleteMutation = useMutationData<{ MsiFileIds: number[] }>({ + url: BASE_URL + API_ENDPOINTS.APP_VERSION.DELETE_FILES, + method: "DELETE", + invalidate: [["software-version"]], + onSuccess: () => toast.success("Xóa phần mềm thành công!"), + onError: (error) => { + console.error("Delete error:", error); + toast.error("Xóa phần mềm thất bại!"); + }, + }); + // Cột bảng const columns: ColumnDef[] = [ { accessorKey: "version", header: "Phiên bản" }, @@ -171,6 +182,30 @@ function AppsComponent() { } }; + const handleDelete = async () => { + 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 file để xóa!"); + return; + } + + const MsiFileIds = selectedRows.map((row: any) => row.original.id); + + try { + await deleteMutation.mutateAsync({ + data: { MsiFileIds }, + }); + } catch (e) { + console.error("Delete error:", e); + toast.error("Có lỗi xảy ra khi xóa!"); + } + }; + return ( title="Quản lý phần mềm" @@ -181,8 +216,10 @@ function AppsComponent() { onUpload={handleUpload} onUpdate={handleInstall} onDownload={handleDonwload} + onDelete={handleDelete} updateLoading={installMutation.isPending} downloadLoading={downloadMutation.isPending} + deleteLoading={deleteMutation.isPending} onTableInit={setTable} rooms={roomData} /> diff --git a/src/template/app-manager-template.tsx b/src/template/app-manager-template.tsx index d7a6aea..c37e78d 100644 --- a/src/template/app-manager-template.tsx +++ b/src/template/app-manager-template.tsx @@ -7,10 +7,11 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { FileText, Building2 } from "lucide-react"; +import { FileText, Building2, Download } from "lucide-react"; import { FormDialog } from "@/components/dialogs/form-dialog"; import { VersionTable } from "@/components/tables/version-table"; import { RequestUpdateMenu } from "@/components/menu/request-update-menu"; +import { DeleteButton } from "@/components/buttons/delete-button"; import type { AxiosProgressEvent } from "axios"; import { useState } from "react"; import { SelectDialog } from "@/components/dialogs/select-dialog"; @@ -34,6 +35,8 @@ interface AppManagerTemplateProps { updateLoading?: boolean; onDownload?: (targetNames: string[]) => Promise | void; downloadLoading?: boolean; + onDelete?: () => Promise | void; + deleteLoading?: boolean; onTableInit?: (table: any) => void; rooms?: Room[]; devices?: string[]; @@ -50,6 +53,8 @@ export function AppManagerTemplate({ updateLoading, onDownload, downloadLoading, + onDelete, + deleteLoading, onTableInit, rooms = [], devices = [], @@ -133,27 +138,45 @@ export function AppManagerTemplate({ /> - {onUpdate && ( - - - {onDownload && ( + {(onUpdate || onDelete) && ( + +
{ - if (!onDownload) return; - const roomIds = rooms.map((room) => - typeof room === "string" ? room : room.name - ); - const allTargets = [...roomIds, ...devices]; - onDownload(allTargets); - }} - loading={downloadLoading} + onUpdateDevice={openDeviceDialog} + onUpdateRoom={openRoomDialog} + onUpdateAll={handleUpdateAll} + loading={updateLoading} + label="Cài đặt" + deviceLabel="Cài đặt thiết bị cụ thể" + roomLabel="Cài đặt theo phòng" + allLabel="Cài đặt tất cả thiết bị" + /> + {onDownload && ( + { + if (!onDownload) return; + const roomIds = rooms.map((room) => + typeof room === "string" ? room : room.name + ); + const allTargets = [...roomIds, ...devices]; + onDownload(allTargets); + }} + loading={downloadLoading} + label="Tải xuống" + deviceLabel="Tải xuống thiết bị cụ thể" + roomLabel="Tải xuống theo phòng" + allLabel="Tải xuống tất cả thiết bị" + icon={} + /> + )} +
+ {onDelete && ( + )}
@@ -167,6 +190,7 @@ export function AppManagerTemplate({ onClose={() => { setDialogOpen(false); setDialogType(null); + setTimeout(() => window.location.reload(), 500); }} title="Chọn phòng" description="Chọn các phòng cần cập nhật" @@ -194,6 +218,7 @@ export function AppManagerTemplate({ onClose={() => { setDialogOpen(false); setDialogType(null); + setTimeout(() => window.location.reload(), 500); }} rooms={rooms} fetchDevices={fetchDevicesFromRoom} @@ -223,6 +248,7 @@ export function AppManagerTemplate({ onClose={() => { setDialogOpen(false); setDialogType(null); + setTimeout(() => window.location.reload(), 500); }} title="Chọn phòng" description="Chọn các phòng để tải file xuống" @@ -250,6 +276,7 @@ export function AppManagerTemplate({ onClose={() => { setDialogOpen(false); setDialogType(null); + setTimeout(() => window.location.reload(), 500); }} rooms={rooms} fetchDevices={fetchDevicesFromRoom}