fix UI freeze problem
This commit is contained in:
parent
3b1865c21d
commit
451eed4c65
81
src/components/buttons/delete-button.tsx
Normal file
81
src/components/buttons/delete-button.tsx
Normal file
|
|
@ -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<void>;
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
disabled={loading || disabled}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
<DialogDescription>{description}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
disabled={isConfirming}
|
||||||
|
>
|
||||||
|
Hủy
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleConfirm}
|
||||||
|
disabled={isConfirming || loading}
|
||||||
|
>
|
||||||
|
{isConfirming ? "Đang xóa..." : "Xóa"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,13 @@ export function UploadVersionForm({ onSubmit, closeDialog }: UploadVersionFormPr
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [isDone, setIsDone] = 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({
|
const form = useForm({
|
||||||
defaultValues: { files: new DataTransfer().files, newVersion: "" },
|
defaultValues: { files: new DataTransfer().files, newVersion: "" },
|
||||||
onSubmit: async ({ value }) => {
|
onSubmit: async ({ value }) => {
|
||||||
|
|
@ -25,6 +32,15 @@ export function UploadVersionForm({ onSubmit, closeDialog }: UploadVersionFormPr
|
||||||
return;
|
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 {
|
try {
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
setUploadPercent(0);
|
setUploadPercent(0);
|
||||||
|
|
@ -81,9 +97,13 @@ export function UploadVersionForm({ onSubmit, closeDialog }: UploadVersionFormPr
|
||||||
<Label>File</Label>
|
<Label>File</Label>
|
||||||
<Input
|
<Input
|
||||||
type="file"
|
type="file"
|
||||||
|
accept=".exe,.apk,.conf,.json,.xml,.setting,.lnk,.url,.seb"
|
||||||
onChange={(e) => e.target.files && field.handleChange(e.target.files)}
|
onChange={(e) => e.target.files && field.handleChange(e.target.files)}
|
||||||
disabled={isUploading || isDone}
|
disabled={isUploading || isDone}
|
||||||
/>
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Chỉ chấp nhận file: .exe, .apk, .conf, .json, .xml, .setting, .lnk, .url, .seb
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</form.Field>
|
</form.Field>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,11 @@ interface RequestUpdateMenuProps {
|
||||||
onUpdateRoom: () => void;
|
onUpdateRoom: () => void;
|
||||||
onUpdateAll: () => void;
|
onUpdateAll: () => void;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
label?: string;
|
||||||
|
deviceLabel?: string;
|
||||||
|
roomLabel?: string;
|
||||||
|
allLabel?: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RequestUpdateMenu({
|
export function RequestUpdateMenu({
|
||||||
|
|
@ -21,6 +26,11 @@ export function RequestUpdateMenu({
|
||||||
onUpdateRoom,
|
onUpdateRoom,
|
||||||
onUpdateAll,
|
onUpdateAll,
|
||||||
loading,
|
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) {
|
}: RequestUpdateMenuProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
|
@ -58,11 +68,13 @@ export function RequestUpdateMenu({
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Loader2 className="h-4 w-4 animate-spin text-gray-600" />
|
<Loader2 className="h-4 w-4 animate-spin text-gray-600" />
|
||||||
|
) : icon ? (
|
||||||
|
<div className="h-4 w-4 text-gray-600">{icon}</div>
|
||||||
) : (
|
) : (
|
||||||
<RefreshCw className="h-4 w-4 text-gray-600 transition-transform duration-300 group-hover:rotate-180" />
|
<RefreshCw className="h-4 w-4 text-gray-600 transition-transform duration-300 group-hover:rotate-180" />
|
||||||
)}
|
)}
|
||||||
<span className="text-sm font-semibold">
|
<span className="text-sm font-semibold">
|
||||||
{loading ? "Đang gửi..." : "Cập nhật"}
|
{loading ? "Đang gửi..." : label}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown className="h-4 w-4 text-gray-600 transition-transform duration-200 group-data-[state=open]:rotate-180" />
|
<ChevronDown className="h-4 w-4 text-gray-600 transition-transform duration-200 group-data-[state=open]:rotate-180" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -73,17 +85,17 @@ export function RequestUpdateMenu({
|
||||||
<DropdownMenuContent align="start" className="w-56">
|
<DropdownMenuContent align="start" className="w-56">
|
||||||
<DropdownMenuItem onClick={handleUpdateDevice} disabled={loading}>
|
<DropdownMenuItem onClick={handleUpdateDevice} disabled={loading}>
|
||||||
<RefreshCw className="h-4 w-4 mr-2" />
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
<span>Cập nhật thiết bị cụ thể</span>
|
<span>{deviceLabel}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={handleUpdateRoom} disabled={loading}>
|
<DropdownMenuItem onClick={handleUpdateRoom} disabled={loading}>
|
||||||
<RefreshCw className="h-4 w-4 mr-2" />
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
<span>Cập nhật theo phòng</span>
|
<span>{roomLabel}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={handleUpdateAll} disabled={loading}>
|
<DropdownMenuItem onClick={handleUpdateAll} disabled={loading}>
|
||||||
<RefreshCw className="h-4 w-4 mr-2" />
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
<span>Cập nhật tất cả thiết bị</span>
|
<span>{allLabel}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ interface VersionTableProps<TData> {
|
||||||
data: TData[];
|
data: TData[];
|
||||||
columns: ColumnDef<TData, any>[];
|
columns: ColumnDef<TData, any>[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
onTableInit?: (table: any) => void; // <-- thêm
|
onTableInit?: (table: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VersionTable<TData>({
|
export function VersionTable<TData>({
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ export const API_ENDPOINTS = {
|
||||||
ADD_BLACKLIST: `${BASE_URL}/AppVersion/blacklist/add`,
|
ADD_BLACKLIST: `${BASE_URL}/AppVersion/blacklist/add`,
|
||||||
DELETE_BLACKLIST: (appId: number) => `${BASE_URL}/AppVersion/blacklist/remove/${appId}`,
|
DELETE_BLACKLIST: (appId: number) => `${BASE_URL}/AppVersion/blacklist/remove/${appId}`,
|
||||||
UPDATE_BLACKLIST: (appId: string) => `${BASE_URL}/AppVersion/blacklist/update/${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`,
|
REQUEST_UPDATE_BLACKLIST: `${BASE_URL}/AppVersion/blacklist/request-update`,
|
||||||
},
|
},
|
||||||
DEVICE_COMM: {
|
DEVICE_COMM: {
|
||||||
|
|
|
||||||
|
|
@ -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
|
// Cột bảng
|
||||||
const columns: ColumnDef<Version>[] = [
|
const columns: ColumnDef<Version>[] = [
|
||||||
{ accessorKey: "version", header: "Phiên bản" },
|
{ 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 (
|
return (
|
||||||
<AppManagerTemplate<Version>
|
<AppManagerTemplate<Version>
|
||||||
title="Quản lý phần mềm"
|
title="Quản lý phần mềm"
|
||||||
|
|
@ -181,8 +216,10 @@ function AppsComponent() {
|
||||||
onUpload={handleUpload}
|
onUpload={handleUpload}
|
||||||
onUpdate={handleInstall}
|
onUpdate={handleInstall}
|
||||||
onDownload={handleDonwload}
|
onDownload={handleDonwload}
|
||||||
|
onDelete={handleDelete}
|
||||||
updateLoading={installMutation.isPending}
|
updateLoading={installMutation.isPending}
|
||||||
downloadLoading={downloadMutation.isPending}
|
downloadLoading={downloadMutation.isPending}
|
||||||
|
deleteLoading={deleteMutation.isPending}
|
||||||
onTableInit={setTable}
|
onTableInit={setTable}
|
||||||
rooms={roomData}
|
rooms={roomData}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,11 @@ import {
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} 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 { FormDialog } from "@/components/dialogs/form-dialog";
|
||||||
import { VersionTable } from "@/components/tables/version-table";
|
import { VersionTable } from "@/components/tables/version-table";
|
||||||
import { RequestUpdateMenu } from "@/components/menu/request-update-menu";
|
import { RequestUpdateMenu } from "@/components/menu/request-update-menu";
|
||||||
|
import { DeleteButton } from "@/components/buttons/delete-button";
|
||||||
import type { AxiosProgressEvent } from "axios";
|
import type { AxiosProgressEvent } from "axios";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { SelectDialog } from "@/components/dialogs/select-dialog";
|
import { SelectDialog } from "@/components/dialogs/select-dialog";
|
||||||
|
|
@ -34,6 +35,8 @@ interface AppManagerTemplateProps<TData> {
|
||||||
updateLoading?: boolean;
|
updateLoading?: boolean;
|
||||||
onDownload?: (targetNames: string[]) => Promise<void> | void;
|
onDownload?: (targetNames: string[]) => Promise<void> | void;
|
||||||
downloadLoading?: boolean;
|
downloadLoading?: boolean;
|
||||||
|
onDelete?: () => Promise<void> | void;
|
||||||
|
deleteLoading?: boolean;
|
||||||
onTableInit?: (table: any) => void;
|
onTableInit?: (table: any) => void;
|
||||||
rooms?: Room[];
|
rooms?: Room[];
|
||||||
devices?: string[];
|
devices?: string[];
|
||||||
|
|
@ -50,6 +53,8 @@ export function AppManagerTemplate<TData>({
|
||||||
updateLoading,
|
updateLoading,
|
||||||
onDownload,
|
onDownload,
|
||||||
downloadLoading,
|
downloadLoading,
|
||||||
|
onDelete,
|
||||||
|
deleteLoading,
|
||||||
onTableInit,
|
onTableInit,
|
||||||
rooms = [],
|
rooms = [],
|
||||||
devices = [],
|
devices = [],
|
||||||
|
|
@ -133,27 +138,45 @@ export function AppManagerTemplate<TData>({
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
{onUpdate && (
|
{(onUpdate || onDelete) && (
|
||||||
<CardFooter className="gap-2">
|
<CardFooter className="flex items-center justify-between gap-4">
|
||||||
<RequestUpdateMenu
|
<div className="flex gap-2">
|
||||||
onUpdateDevice={openDeviceDialog}
|
|
||||||
onUpdateRoom={openRoomDialog}
|
|
||||||
onUpdateAll={handleUpdateAll}
|
|
||||||
loading={updateLoading}
|
|
||||||
/>
|
|
||||||
{onDownload && (
|
|
||||||
<RequestUpdateMenu
|
<RequestUpdateMenu
|
||||||
onUpdateDevice={openDownloadDeviceDialog}
|
onUpdateDevice={openDeviceDialog}
|
||||||
onUpdateRoom={openDownloadRoomDialog}
|
onUpdateRoom={openRoomDialog}
|
||||||
onUpdateAll={() => {
|
onUpdateAll={handleUpdateAll}
|
||||||
if (!onDownload) return;
|
loading={updateLoading}
|
||||||
const roomIds = rooms.map((room) =>
|
label="Cài đặt"
|
||||||
typeof room === "string" ? room : room.name
|
deviceLabel="Cài đặt thiết bị cụ thể"
|
||||||
);
|
roomLabel="Cài đặt theo phòng"
|
||||||
const allTargets = [...roomIds, ...devices];
|
allLabel="Cài đặt tất cả thiết bị"
|
||||||
onDownload(allTargets);
|
/>
|
||||||
}}
|
{onDownload && (
|
||||||
loading={downloadLoading}
|
<RequestUpdateMenu
|
||||||
|
onUpdateDevice={openDownloadDeviceDialog}
|
||||||
|
onUpdateRoom={openDownloadRoomDialog}
|
||||||
|
onUpdateAll={() => {
|
||||||
|
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={<Download className="h-4 w-4" />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{onDelete && (
|
||||||
|
<DeleteButton
|
||||||
|
onClick={onDelete}
|
||||||
|
loading={deleteLoading}
|
||||||
|
disabled={false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
|
|
@ -167,6 +190,7 @@ export function AppManagerTemplate<TData>({
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
setDialogType(null);
|
setDialogType(null);
|
||||||
|
setTimeout(() => window.location.reload(), 500);
|
||||||
}}
|
}}
|
||||||
title="Chọn phòng"
|
title="Chọn phòng"
|
||||||
description="Chọn các phòng cần cập nhật"
|
description="Chọn các phòng cần cập nhật"
|
||||||
|
|
@ -194,6 +218,7 @@ export function AppManagerTemplate<TData>({
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
setDialogType(null);
|
setDialogType(null);
|
||||||
|
setTimeout(() => window.location.reload(), 500);
|
||||||
}}
|
}}
|
||||||
rooms={rooms}
|
rooms={rooms}
|
||||||
fetchDevices={fetchDevicesFromRoom}
|
fetchDevices={fetchDevicesFromRoom}
|
||||||
|
|
@ -223,6 +248,7 @@ export function AppManagerTemplate<TData>({
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
setDialogType(null);
|
setDialogType(null);
|
||||||
|
setTimeout(() => window.location.reload(), 500);
|
||||||
}}
|
}}
|
||||||
title="Chọn phòng"
|
title="Chọn phòng"
|
||||||
description="Chọn các phòng để tải file xuống"
|
description="Chọn các phòng để tải file xuống"
|
||||||
|
|
@ -250,6 +276,7 @@ export function AppManagerTemplate<TData>({
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
setDialogType(null);
|
setDialogType(null);
|
||||||
|
setTimeout(() => window.location.reload(), 500);
|
||||||
}}
|
}}
|
||||||
rooms={rooms}
|
rooms={rooms}
|
||||||
fetchDevices={fetchDevicesFromRoom}
|
fetchDevices={fetchDevicesFromRoom}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user