Compare commits
No commits in common. "feature_update_button" and "main" have entirely different histories.
feature_up
...
main
180
package-lock.json
generated
180
package-lock.json
generated
|
|
@ -9,7 +9,6 @@
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-progress": "^1.1.7",
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
|
|
@ -1580,41 +1579,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-dropdown-menu": {
|
|
||||||
"version": "2.1.16",
|
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz",
|
|
||||||
"integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@radix-ui/primitive": "1.1.3",
|
|
||||||
"@radix-ui/react-compose-refs": "1.1.2",
|
|
||||||
"@radix-ui/react-context": "1.1.2",
|
|
||||||
"@radix-ui/react-id": "1.1.1",
|
|
||||||
"@radix-ui/react-menu": "2.1.16",
|
|
||||||
"@radix-ui/react-primitive": "2.1.3",
|
|
||||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "*",
|
|
||||||
"@types/react-dom": "*",
|
|
||||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
|
||||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@types/react-dom": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/primitive": {
|
|
||||||
"version": "1.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
|
||||||
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@radix-ui/react-focus-guards": {
|
"node_modules/@radix-ui/react-focus-guards": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
|
||||||
|
|
@ -1696,150 +1660,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-menu": {
|
|
||||||
"version": "2.1.16",
|
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz",
|
|
||||||
"integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@radix-ui/primitive": "1.1.3",
|
|
||||||
"@radix-ui/react-collection": "1.1.7",
|
|
||||||
"@radix-ui/react-compose-refs": "1.1.2",
|
|
||||||
"@radix-ui/react-context": "1.1.2",
|
|
||||||
"@radix-ui/react-direction": "1.1.1",
|
|
||||||
"@radix-ui/react-dismissable-layer": "1.1.11",
|
|
||||||
"@radix-ui/react-focus-guards": "1.1.3",
|
|
||||||
"@radix-ui/react-focus-scope": "1.1.7",
|
|
||||||
"@radix-ui/react-id": "1.1.1",
|
|
||||||
"@radix-ui/react-popper": "1.2.8",
|
|
||||||
"@radix-ui/react-portal": "1.1.9",
|
|
||||||
"@radix-ui/react-presence": "1.1.5",
|
|
||||||
"@radix-ui/react-primitive": "2.1.3",
|
|
||||||
"@radix-ui/react-roving-focus": "1.1.11",
|
|
||||||
"@radix-ui/react-slot": "1.2.3",
|
|
||||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
|
||||||
"aria-hidden": "^1.2.4",
|
|
||||||
"react-remove-scroll": "^2.6.3"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "*",
|
|
||||||
"@types/react-dom": "*",
|
|
||||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
|
||||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@types/react-dom": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/primitive": {
|
|
||||||
"version": "1.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
|
||||||
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-dismissable-layer": {
|
|
||||||
"version": "1.1.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
|
|
||||||
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@radix-ui/primitive": "1.1.3",
|
|
||||||
"@radix-ui/react-compose-refs": "1.1.2",
|
|
||||||
"@radix-ui/react-primitive": "2.1.3",
|
|
||||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
|
||||||
"@radix-ui/react-use-escape-keydown": "1.1.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "*",
|
|
||||||
"@types/react-dom": "*",
|
|
||||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
|
||||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@types/react-dom": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-focus-guards": {
|
|
||||||
"version": "1.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
|
|
||||||
"integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "*",
|
|
||||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-popper": {
|
|
||||||
"version": "1.2.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
|
|
||||||
"integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@floating-ui/react-dom": "^2.0.0",
|
|
||||||
"@radix-ui/react-arrow": "1.1.7",
|
|
||||||
"@radix-ui/react-compose-refs": "1.1.2",
|
|
||||||
"@radix-ui/react-context": "1.1.2",
|
|
||||||
"@radix-ui/react-primitive": "2.1.3",
|
|
||||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
|
||||||
"@radix-ui/react-use-layout-effect": "1.1.1",
|
|
||||||
"@radix-ui/react-use-rect": "1.1.1",
|
|
||||||
"@radix-ui/react-use-size": "1.1.1",
|
|
||||||
"@radix-ui/rect": "1.1.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "*",
|
|
||||||
"@types/react-dom": "*",
|
|
||||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
|
||||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@types/react-dom": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-presence": {
|
|
||||||
"version": "1.1.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
|
|
||||||
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@radix-ui/react-compose-refs": "1.1.2",
|
|
||||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "*",
|
|
||||||
"@types/react-dom": "*",
|
|
||||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
|
||||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@types/react-dom": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@radix-ui/react-popover": {
|
"node_modules/@radix-ui/react-popover": {
|
||||||
"version": "1.1.15",
|
"version": "1.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz",
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-progress": "^1.1.7",
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
|
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
import { useState } from "react";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { useForm, formOptions } from "@tanstack/react-form";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
interface AddBlacklistDialogProps {
|
|
||||||
onAdded?: () => void; // callback để refresh danh sách sau khi thêm
|
|
||||||
}
|
|
||||||
|
|
||||||
const formOpts = formOptions({
|
|
||||||
defaultValues: { appName: "", processName: "" },
|
|
||||||
});
|
|
||||||
|
|
||||||
export function AddBlacklistDialog({ onAdded }: AddBlacklistDialogProps) {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const [isDone, setIsDone] = useState(false);
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
...formOpts,
|
|
||||||
onSubmit: async ({ value }) => {
|
|
||||||
if (!value.appName || !value.processName) {
|
|
||||||
toast.error("Vui lòng nhập đầy đủ tên ứng dụng và tiến trình");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
|
|
||||||
await axios.post("/api/appversions/add-blacklist", {
|
|
||||||
appName: value.appName,
|
|
||||||
processName: value.processName,
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success("Đã thêm vào blacklist!");
|
|
||||||
setIsDone(true);
|
|
||||||
|
|
||||||
if (onAdded) onAdded();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
toast.error("Không thể thêm vào blacklist");
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleDialogClose = (open: boolean) => {
|
|
||||||
if (isSubmitting) return;
|
|
||||||
setIsOpen(open);
|
|
||||||
if (!open) {
|
|
||||||
setIsDone(false);
|
|
||||||
form.reset();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={handleDialogClose}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button>Thêm vào Blacklist</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Thêm ứng dụng vào danh sách cấm</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<form
|
|
||||||
className="space-y-4"
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
form.handleSubmit();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<form.Field name="appName">
|
|
||||||
{(field) => (
|
|
||||||
<div>
|
|
||||||
<Label>Tên ứng dụng</Label>
|
|
||||||
<Input
|
|
||||||
value={field.state.value}
|
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
|
||||||
placeholder="Ví dụ: Google Chrome"
|
|
||||||
disabled={isSubmitting || isDone}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form.Field>
|
|
||||||
|
|
||||||
<form.Field name="processName">
|
|
||||||
{(field) => (
|
|
||||||
<div>
|
|
||||||
<Label>Tên tiến trình</Label>
|
|
||||||
<Input
|
|
||||||
value={field.state.value}
|
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
|
||||||
placeholder="chrome.exe"
|
|
||||||
disabled={isSubmitting || isDone}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form.Field>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
{!isDone ? (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => handleDialogClose(false)}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
Hủy
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
|
||||||
{isSubmitting ? "Đang thêm..." : "Thêm"}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Button type="button" onClick={() => handleDialogClose(false)}>
|
|
||||||
Hoàn tất
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -11,35 +11,23 @@ export function DeviceGrid({ devices }: { devices: any[] }) {
|
||||||
if (number > 0 && number <= 40) deviceMap.set(number, device);
|
if (number > 0 && number <= 40) deviceMap.set(number, device);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const computersPerRow = 8;
|
||||||
const totalRows = 5;
|
const totalRows = 5;
|
||||||
|
|
||||||
const renderRow = (rowIndex: number) => {
|
const renderRow = (rowIndex: number) => {
|
||||||
// Trái: 1–20
|
const start = rowIndex * computersPerRow + 1;
|
||||||
const leftStart = rowIndex * 4 + 1;
|
|
||||||
// Phải: 21–40
|
|
||||||
const rightStart = 21 + rowIndex * 4;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={rowIndex} className="flex items-center justify-center gap-3">
|
<div key={rowIndex} className="flex items-center justify-center gap-3">
|
||||||
{/* Bên trái (1–20) */}
|
|
||||||
{Array.from({ length: 4 }).map((_, i) => {
|
{Array.from({ length: 4 }).map((_, i) => {
|
||||||
const pos = leftStart + i;
|
const pos = start + i;
|
||||||
return (
|
return <ComputerCard key={pos} device={deviceMap.get(pos)} position={pos} />;
|
||||||
<ComputerCard key={pos} device={deviceMap.get(pos)} position={pos} />
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Đường chia giữa */}
|
|
||||||
<div className="w-32 flex items-center justify-center">
|
<div className="w-32 flex items-center justify-center">
|
||||||
<div className="h-px w-full bg-border border-t-2 border-dashed" />
|
<div className="h-px w-full bg-border border-t-2 border-dashed" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bên phải (21–40) */}
|
|
||||||
{Array.from({ length: 4 }).map((_, i) => {
|
{Array.from({ length: 4 }).map((_, i) => {
|
||||||
const pos = rightStart + i;
|
const pos = start + i + 4;
|
||||||
return (
|
return <ComputerCard key={pos} device={deviceMap.get(pos)} position={pos} />;
|
||||||
<ComputerCard key={pos} device={deviceMap.get(pos)} position={pos} />
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import { Loader2, RefreshCw, ChevronDown } from "lucide-react";
|
|
||||||
|
|
||||||
interface RequestUpdateMenuProps {
|
|
||||||
onUpdateDevice: () => void;
|
|
||||||
onUpdateRoom: () => void;
|
|
||||||
onUpdateAll: () => void;
|
|
||||||
loading?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RequestUpdateMenu({
|
|
||||||
onUpdateDevice,
|
|
||||||
onUpdateRoom,
|
|
||||||
onUpdateAll,
|
|
||||||
loading,
|
|
||||||
}: RequestUpdateMenuProps) {
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
disabled={loading}
|
|
||||||
className="group relative overflow-hidden border-2 border-gray-300 bg-white text-gray-800 font-medium px-6 py-2.5 rounded-lg transition-all duration-300 hover:border-gray-400 hover:bg-gray-50 hover:shadow-lg hover:shadow-gray-200/50 hover:-translate-y-0.5 disabled:opacity-70 disabled:cursor-not-allowed disabled:transform-none disabled:shadow-none"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{loading ? (
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin text-gray-600" />
|
|
||||||
) : (
|
|
||||||
<RefreshCw className="h-4 w-4 text-gray-600 transition-transform duration-300 group-hover:rotate-180" />
|
|
||||||
)}
|
|
||||||
<span className="text-sm font-semibold">
|
|
||||||
{loading ? "Đang gửi..." : "Cập nhật"}
|
|
||||||
</span>
|
|
||||||
<ChevronDown className="h-4 w-4 text-gray-600 transition-transform duration-200 group-data-[state=open]:rotate-180" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-gray-100/30 to-transparent transition-transform duration-700 group-hover:translate-x-full" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="start" className="w-56">
|
|
||||||
<DropdownMenuItem onClick={onUpdateDevice} disabled={loading}>
|
|
||||||
<RefreshCw className="h-4 w-4 mr-2" />
|
|
||||||
<span>Cập nhật thiết bị cụ thể</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem onClick={onUpdateRoom} disabled={loading}>
|
|
||||||
<RefreshCw className="h-4 w-4 mr-2" />
|
|
||||||
<span>Cập nhật theo phòng</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem onClick={onUpdateAll} disabled={loading}>
|
|
||||||
<RefreshCw className="h-4 w-4 mr-2" />
|
|
||||||
<span>Cập nhật tất cả thiết bị</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
98
src/components/room-select-dialog.tsx
Normal file
98
src/components/room-select-dialog.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState } from "react"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import { Check, Home } from "lucide-react"
|
||||||
|
|
||||||
|
interface RoomSelectDialogProps {
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
rooms: string[]
|
||||||
|
onConfirm: (roomName: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RoomSelectDialog({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
rooms,
|
||||||
|
onConfirm,
|
||||||
|
}: RoomSelectDialogProps) {
|
||||||
|
const [selectedRoom, setSelectedRoom] = useState<string>("")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader className="text-center pb-4">
|
||||||
|
<div className="mx-auto w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mb-3">
|
||||||
|
<Home className="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<DialogTitle className="text-xl font-semibold">
|
||||||
|
Chọn phòng để cập nhật
|
||||||
|
</DialogTitle>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Vui lòng chọn phòng để gửi lệnh cập nhật
|
||||||
|
</p>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="py-3">
|
||||||
|
<RadioGroup
|
||||||
|
value={selectedRoom}
|
||||||
|
onValueChange={setSelectedRoom}
|
||||||
|
className="space-y-3"
|
||||||
|
>
|
||||||
|
{rooms.map((room) => (
|
||||||
|
<div
|
||||||
|
key={room}
|
||||||
|
className="flex items-center justify-between p-3 rounded-lg border border-border hover:border-primary/60 hover:bg-accent/50 transition-all duration-200 cursor-pointer"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<RadioGroupItem value={room} id={room} />
|
||||||
|
<Label
|
||||||
|
htmlFor={room}
|
||||||
|
className="font-medium cursor-pointer hover:text-primary"
|
||||||
|
>
|
||||||
|
{room}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedRoom === room && (
|
||||||
|
<div className="w-5 h-5 bg-primary rounded-full flex items-center justify-center">
|
||||||
|
<Check className="w-3 h-3 text-primary-foreground" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="gap-2 pt-4">
|
||||||
|
<Button variant="outline" onClick={onClose} className="flex-1 sm:flex-none">
|
||||||
|
Hủy
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedRoom) {
|
||||||
|
onConfirm(selectedRoom)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!selectedRoom}
|
||||||
|
className="flex-1 sm:flex-none"
|
||||||
|
>
|
||||||
|
<Check className="w-4 h-4 mr-2" />
|
||||||
|
Xác nhận
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
import { useEffect, useState, useMemo } from "react"
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogFooter,
|
|
||||||
} from "@/components/ui/dialog"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
|
||||||
import { Label } from "@/components/ui/label"
|
|
||||||
import { Check, Search } from "lucide-react"
|
|
||||||
import { Input } from "@/components/ui/input"
|
|
||||||
|
|
||||||
interface SelectDialogProps {
|
|
||||||
open: boolean
|
|
||||||
onClose: () => void
|
|
||||||
items: string[] // danh sách chung: có thể là devices hoặc rooms
|
|
||||||
title?: string // tiêu đề động
|
|
||||||
description?: string // mô tả ngắn
|
|
||||||
icon?: React.ReactNode // icon thay đổi tùy loại
|
|
||||||
onConfirm: (selected: string[]) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SelectDialog({
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
items,
|
|
||||||
title = "Chọn mục",
|
|
||||||
description = "Bạn có thể chọn nhiều mục để thao tác",
|
|
||||||
icon,
|
|
||||||
onConfirm,
|
|
||||||
}: SelectDialogProps) {
|
|
||||||
const [selectedItems, setSelectedItems] = useState<string[]>([])
|
|
||||||
const [search, setSearch] = useState("")
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) {
|
|
||||||
setSelectedItems([])
|
|
||||||
setSearch("")
|
|
||||||
}
|
|
||||||
}, [open])
|
|
||||||
|
|
||||||
const toggleItem = (item: string) => {
|
|
||||||
setSelectedItems((prev) =>
|
|
||||||
prev.includes(item)
|
|
||||||
? prev.filter((i) => i !== item)
|
|
||||||
: [...prev, item]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lọc danh sách theo từ khóa
|
|
||||||
const filteredItems = useMemo(() => {
|
|
||||||
return items.filter((item) =>
|
|
||||||
item.toLowerCase().includes(search.toLowerCase())
|
|
||||||
)
|
|
||||||
}, [items, search])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={onClose}>
|
|
||||||
<DialogContent className="sm:max-w-md">
|
|
||||||
<DialogHeader className="text-center pb-4">
|
|
||||||
<div className="mx-auto w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mb-3">
|
|
||||||
{icon ?? <Search className="w-6 h-6 text-primary" />}
|
|
||||||
</div>
|
|
||||||
<DialogTitle className="text-xl font-semibold">{title}</DialogTitle>
|
|
||||||
<p className="text-sm text-muted-foreground mt-1">{description}</p>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
{/* 🔍 Thanh tìm kiếm */}
|
|
||||||
<div className="relative mb-3">
|
|
||||||
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
placeholder="Tìm kiếm..."
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
|
||||||
className="pl-9"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Danh sách các item */}
|
|
||||||
<div className="py-3 space-y-3 max-h-64 overflow-y-auto">
|
|
||||||
{filteredItems.length > 0 ? (
|
|
||||||
filteredItems.map((item) => (
|
|
||||||
<div
|
|
||||||
key={item}
|
|
||||||
className="flex items-center justify-between p-3 rounded-lg border border-border hover:border-primary/60 hover:bg-accent/50 transition-all duration-200 cursor-pointer"
|
|
||||||
onClick={() => toggleItem(item)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Checkbox
|
|
||||||
checked={selectedItems.includes(item)}
|
|
||||||
onCheckedChange={() => toggleItem(item)}
|
|
||||||
/>
|
|
||||||
<Label className="font-medium cursor-pointer hover:text-primary">
|
|
||||||
{item}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedItems.includes(item) && (
|
|
||||||
<div className="w-5 h-5 bg-primary rounded-full flex items-center justify-center">
|
|
||||||
<Check className="w-3 h-3 text-primary-foreground" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<p className="text-center text-sm text-muted-foreground py-4">
|
|
||||||
Không tìm thấy kết quả
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter className="gap-2 pt-4">
|
|
||||||
<Button variant="outline" onClick={onClose} className="flex-1 sm:flex-none">
|
|
||||||
Hủy
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
if (selectedItems.length > 0) {
|
|
||||||
onConfirm(selectedItems)
|
|
||||||
onClose()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={selectedItems.length === 0}
|
|
||||||
className="flex-1 sm:flex-none"
|
|
||||||
>
|
|
||||||
<Check className="w-4 h-4 mr-2" />
|
|
||||||
Xác nhận ({selectedItems.length})
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,255 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
|
||||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function DropdownMenu({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
|
||||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuPortal({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Trigger
|
|
||||||
data-slot="dropdown-menu-trigger"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuContent({
|
|
||||||
className,
|
|
||||||
sideOffset = 4,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Portal>
|
|
||||||
<DropdownMenuPrimitive.Content
|
|
||||||
data-slot="dropdown-menu-content"
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</DropdownMenuPrimitive.Portal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuGroup({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuItem({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
variant = "default",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
|
||||||
inset?: boolean
|
|
||||||
variant?: "default" | "destructive"
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Item
|
|
||||||
data-slot="dropdown-menu-item"
|
|
||||||
data-inset={inset}
|
|
||||||
data-variant={variant}
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuCheckboxItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
checked,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
|
||||||
data-slot="dropdown-menu-checkbox-item"
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
checked={checked}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<CheckIcon className="size-4" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuRadioGroup({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.RadioGroup
|
|
||||||
data-slot="dropdown-menu-radio-group"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuRadioItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.RadioItem
|
|
||||||
data-slot="dropdown-menu-radio-item"
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<CircleIcon className="size-2 fill-current" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuLabel({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
|
||||||
inset?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Label
|
|
||||||
data-slot="dropdown-menu-label"
|
|
||||||
data-inset={inset}
|
|
||||||
className={cn(
|
|
||||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSeparator({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Separator
|
|
||||||
data-slot="dropdown-menu-separator"
|
|
||||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuShortcut({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"span">) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
data-slot="dropdown-menu-shortcut"
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSub({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
|
||||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSubTrigger({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
|
||||||
data-slot="dropdown-menu-sub-trigger"
|
|
||||||
data-inset={inset}
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronRightIcon className="ml-auto size-4" />
|
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSubContent({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.SubContent
|
|
||||||
data-slot="dropdown-menu-sub-content"
|
|
||||||
className={cn(
|
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
DropdownMenuRadioItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuShortcut,
|
|
||||||
DropdownMenuSub,
|
|
||||||
DropdownMenuSubTrigger,
|
|
||||||
DropdownMenuSubContent,
|
|
||||||
}
|
|
||||||
|
|
@ -29,5 +29,6 @@ export const API_ENDPOINTS = {
|
||||||
DEVICE_ONLINE: `${BASE_URL}/Sse/events/onlineDevices`,
|
DEVICE_ONLINE: `${BASE_URL}/Sse/events/onlineDevices`,
|
||||||
DEVICE_OFFLINE: `${BASE_URL}/Sse/events/offlineDevices`,
|
DEVICE_OFFLINE: `${BASE_URL}/Sse/events/offlineDevices`,
|
||||||
GET_PROCESSES_LISTS: `${BASE_URL}/Sse/events/processLists`,
|
GET_PROCESSES_LISTS: `${BASE_URL}/Sse/events/processLists`,
|
||||||
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ function AgentsPage() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateMutation = useMutationData<void>({
|
const updateMutation = useMutationData<void>({
|
||||||
url: "",
|
url: "",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
onSuccess: () => toast.success("Đã gửi yêu cầu update!"),
|
onSuccess: () => toast.success("Đã gửi yêu cầu update!"),
|
||||||
onError: () => toast.error("Gửi yêu cầu thất bại!"),
|
onError: () => toast.error("Gửi yêu cầu thất bại!"),
|
||||||
|
|
@ -75,19 +75,13 @@ function AgentsPage() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdate = async (roomNames: string[]) => {
|
// Callback khi chọn phòng update
|
||||||
for (const roomName of roomNames) {
|
const handleUpdate = async (roomName: string) => {
|
||||||
try {
|
return updateMutation.mutateAsync({
|
||||||
await updateMutation.mutateAsync({
|
url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.UPDATE_AGENT(roomName),
|
||||||
url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.UPDATE_AGENT(roomName),
|
method: "POST",
|
||||||
method: "POST",
|
data: undefined,
|
||||||
data: undefined
|
});
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
toast.error(`Gửi yêu cầu thất bại cho ${roomName}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toast.success("Đã gửi yêu cầu update cho các phòng đã chọn!");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Cột bảng
|
// Cột bảng
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ function AppsComponent() {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Callback khi chọn phòng
|
// Callback khi chọn phòng
|
||||||
const handleInstall = async (roomNames: string[]) => {
|
const handleInstall = async (roomName: string) => {
|
||||||
if (!table) {
|
if (!table) {
|
||||||
toast.error("Không thể lấy thông tin bảng!");
|
toast.error("Không thể lấy thông tin bảng!");
|
||||||
return;
|
return;
|
||||||
|
|
@ -127,14 +127,10 @@ function AppsComponent() {
|
||||||
|
|
||||||
const MsiFileIds = selectedRows.map((row: any) => row.original.id);
|
const MsiFileIds = selectedRows.map((row: any) => row.original.id);
|
||||||
|
|
||||||
for (const roomName of roomNames) {
|
return installMutation.mutateAsync({
|
||||||
await installMutation.mutateAsync({
|
url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.DOWNLOAD_MSI(roomName), // set url động
|
||||||
url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.DOWNLOAD_MSI(roomName),
|
data: { MsiFileIds },
|
||||||
data: { MsiFileIds },
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success("Đã gửi yêu cầu cài đặt phần mềm cho các phòng đã chọn!");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export const Route = createFileRoute("/_authenticated/room/$roomName/")({
|
||||||
|
|
||||||
function RoomDetailPage() {
|
function RoomDetailPage() {
|
||||||
const { roomName } = useParams({ from: "/_authenticated/room/$roomName/" });
|
const { roomName } = useParams({ from: "/_authenticated/room/$roomName/" });
|
||||||
const [viewMode, setViewMode] = useState<"grid" | "table">("grid");
|
const [viewMode, setViewMode] = useState<"table" | "grid">("table");
|
||||||
const { data: devices = [] } = useQueryData({
|
const { data: devices = [] } = useQueryData({
|
||||||
queryKey: ["devices", roomName],
|
queryKey: ["devices", roomName],
|
||||||
url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.GET_DEVICE_FROM_ROOM(roomName),
|
url: BASE_URL + API_ENDPOINTS.DEVICE_COMM.GET_DEVICE_FROM_ROOM(roomName),
|
||||||
|
|
@ -33,15 +33,6 @@ function RoomDetailPage() {
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 bg-background rounded-lg p-1 border">
|
<div className="flex items-center gap-2 bg-background rounded-lg p-1 border">
|
||||||
<Button
|
|
||||||
variant={viewMode === "grid" ? "default" : "ghost"}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setViewMode("grid")}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<LayoutGrid className="h-4 w-4" />
|
|
||||||
Sơ đồ
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
variant={viewMode === "table" ? "default" : "ghost"}
|
variant={viewMode === "table" ? "default" : "ghost"}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -51,6 +42,15 @@ function RoomDetailPage() {
|
||||||
<TableIcon className="h-4 w-4" />
|
<TableIcon className="h-4 w-4" />
|
||||||
Bảng
|
Bảng
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={viewMode === "grid" ? "default" : "ghost"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setViewMode("grid")}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<LayoutGrid className="h-4 w-4" />
|
||||||
|
Sơ đồ
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,13 @@ import {
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { FileText, Building2, Monitor } from "lucide-react";
|
import { FileText } from "lucide-react";
|
||||||
import { UploadDialog } from "@/components/upload-dialog";
|
import { UploadDialog } from "@/components/upload-dialog";
|
||||||
import { VersionTable } from "@/components/version-table";
|
import { VersionTable } from "@/components/version-table";
|
||||||
import { RequestUpdateMenu } from "@/components/request-update-menu";
|
import { UpdateButton } from "@/components/update-button";
|
||||||
|
import { RoomSelectDialog } from "@/components/room-select-dialog";
|
||||||
import type { AxiosProgressEvent } from "axios";
|
import type { AxiosProgressEvent } from "axios";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { SelectDialog } from "@/components/select-dialog"; // <-- dùng dialog chung
|
|
||||||
|
|
||||||
interface AppManagerTemplateProps<TData> {
|
interface AppManagerTemplateProps<TData> {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -25,11 +25,10 @@ interface AppManagerTemplateProps<TData> {
|
||||||
fd: FormData,
|
fd: FormData,
|
||||||
config?: { onUploadProgress?: (e: AxiosProgressEvent) => void }
|
config?: { onUploadProgress?: (e: AxiosProgressEvent) => void }
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
onUpdate?: (targetNames: string[]) => Promise<void> | void;
|
onUpdate?: (roomName: string) => void;
|
||||||
updateLoading?: boolean;
|
updateLoading?: boolean;
|
||||||
onTableInit?: (table: any) => void;
|
onTableInit?: (table: any) => void;
|
||||||
rooms?: string[];
|
rooms: string[];
|
||||||
devices?: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppManagerTemplate<TData>({
|
export function AppManagerTemplate<TData>({
|
||||||
|
|
@ -42,57 +41,16 @@ export function AppManagerTemplate<TData>({
|
||||||
onUpdate,
|
onUpdate,
|
||||||
updateLoading,
|
updateLoading,
|
||||||
onTableInit,
|
onTableInit,
|
||||||
rooms = [],
|
rooms,
|
||||||
devices = [],
|
|
||||||
}: AppManagerTemplateProps<TData>) {
|
}: AppManagerTemplateProps<TData>) {
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [dialogType, setDialogType] = useState<"room" | "device" | null>(null);
|
const handleUpdateClick = () => {
|
||||||
|
if (rooms && onUpdate) {
|
||||||
const openRoomDialog = () => {
|
|
||||||
if (rooms.length > 0 && onUpdate) {
|
|
||||||
setDialogType("room");
|
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openDeviceDialog = () => {
|
|
||||||
if (devices.length > 0 && onUpdate) {
|
|
||||||
setDialogType("device");
|
|
||||||
setDialogOpen(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdateAll = async () => {
|
|
||||||
if (!onUpdate) return;
|
|
||||||
const allTargets = [...rooms, ...devices];
|
|
||||||
await onUpdate(allTargets);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDialogProps = () => {
|
|
||||||
if (dialogType === "room") {
|
|
||||||
return {
|
|
||||||
title: "Chọn phòng",
|
|
||||||
description: "Chọn các phòng cần cập nhật",
|
|
||||||
icon: <Building2 className="w-6 h-6 text-primary" />,
|
|
||||||
items: rooms,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (dialogType === "device") {
|
|
||||||
return {
|
|
||||||
title: "Chọn thiết bị",
|
|
||||||
description: "Chọn các thiết bị cần cập nhật",
|
|
||||||
icon: <Monitor className="w-6 h-6 text-primary" />,
|
|
||||||
items: devices,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const dialogProps = getDialogProps();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full px-6 space-y-4">
|
<div className="w-full px-6 space-y-4">
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold">{title}</h1>
|
<h1 className="text-3xl font-bold">{title}</h1>
|
||||||
|
|
@ -101,7 +59,6 @@ export function AppManagerTemplate<TData>({
|
||||||
<UploadDialog onSubmit={onUpload} />
|
<UploadDialog onSubmit={onUpload} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
|
@ -109,7 +66,6 @@ export function AppManagerTemplate<TData>({
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>Tất cả các phiên bản đã tải lên</CardDescription>
|
<CardDescription>Tất cả các phiên bản đã tải lên</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<VersionTable
|
<VersionTable
|
||||||
data={data}
|
data={data}
|
||||||
|
|
@ -118,35 +74,28 @@ export function AppManagerTemplate<TData>({
|
||||||
onTableInit={onTableInit}
|
onTableInit={onTableInit}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
{onUpdate && (
|
{onUpdate && (
|
||||||
<CardFooter>
|
<CardFooter>
|
||||||
<RequestUpdateMenu
|
<UpdateButton onClick={handleUpdateClick} loading={updateLoading} />
|
||||||
onUpdateDevice={openDeviceDialog}
|
<UpdateButton
|
||||||
onUpdateRoom={openRoomDialog}
|
onClick={() => onUpdate("All")}
|
||||||
onUpdateAll={handleUpdateAll}
|
|
||||||
loading={updateLoading}
|
loading={updateLoading}
|
||||||
|
label="Cập nhật tất cả thiết bị"
|
||||||
/>
|
/>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
)}
|
)}
|
||||||
|
{rooms && onUpdate && (
|
||||||
|
<RoomSelectDialog
|
||||||
|
open={dialogOpen}
|
||||||
|
onClose={() => setDialogOpen(false)}
|
||||||
|
rooms={rooms}
|
||||||
|
onConfirm={(roomName) => {
|
||||||
|
onUpdate(roomName);
|
||||||
|
setDialogOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 🧩 SelectDialog tái sử dụng */}
|
|
||||||
{dialogProps && (
|
|
||||||
<SelectDialog
|
|
||||||
open={dialogOpen}
|
|
||||||
onClose={() => setDialogOpen(false)}
|
|
||||||
title={dialogProps.title}
|
|
||||||
description={dialogProps.description}
|
|
||||||
icon={dialogProps.icon}
|
|
||||||
items={dialogProps.items}
|
|
||||||
onConfirm={async (selectedItems) => {
|
|
||||||
if (!onUpdate) return;
|
|
||||||
await onUpdate(selectedItems);
|
|
||||||
setDialogOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
|
|
@ -7,9 +9,9 @@ import {
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card"
|
} from "@/components/ui/card"
|
||||||
import { Terminal, Building2, Monitor } from "lucide-react"
|
import { UpdateButton } from "@/components/update-button"
|
||||||
import { RequestUpdateMenu } from "@/components/request-update-menu"
|
import { Terminal } from "lucide-react"
|
||||||
import { SelectDialog } from "@/components/select-dialog"
|
import { RoomSelectDialog } from "@/components/room-select-dialog"
|
||||||
|
|
||||||
interface FormSubmitTemplateProps {
|
interface FormSubmitTemplateProps {
|
||||||
title: string
|
title: string
|
||||||
|
|
@ -19,10 +21,9 @@ interface FormSubmitTemplateProps {
|
||||||
command: string
|
command: string
|
||||||
setCommand: (val: string) => void
|
setCommand: (val: string) => void
|
||||||
}) => React.ReactNode
|
}) => React.ReactNode
|
||||||
onSubmit?: (target: string, command: string) => void | Promise<void>
|
onSubmit?: (roomName: string, command: string) => void
|
||||||
submitLoading?: boolean
|
submitLoading?: boolean
|
||||||
rooms?: string[]
|
rooms?: string[]
|
||||||
devices?: string[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FormSubmitTemplate({
|
export function FormSubmitTemplate({
|
||||||
|
|
@ -33,56 +34,16 @@ export function FormSubmitTemplate({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
submitLoading,
|
submitLoading,
|
||||||
rooms = [],
|
rooms = [],
|
||||||
devices = [],
|
|
||||||
}: FormSubmitTemplateProps) {
|
}: FormSubmitTemplateProps) {
|
||||||
const [command, setCommand] = useState("")
|
|
||||||
const [dialogOpen, setDialogOpen] = useState(false)
|
const [dialogOpen, setDialogOpen] = useState(false)
|
||||||
const [dialogType, setDialogType] = useState<"room" | "device" | null>(null)
|
const [command, setCommand] = useState("")
|
||||||
|
|
||||||
const openRoomDialog = () => {
|
const handleClick = () => {
|
||||||
if (rooms.length > 0 && onSubmit) {
|
if (rooms.length > 0 && onSubmit) {
|
||||||
setDialogType("room")
|
|
||||||
setDialogOpen(true)
|
setDialogOpen(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const openDeviceDialog = () => {
|
|
||||||
if (devices.length > 0 && onSubmit) {
|
|
||||||
setDialogType("device")
|
|
||||||
setDialogOpen(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmitAll = () => {
|
|
||||||
if (!onSubmit) return
|
|
||||||
const allTargets = [...rooms, ...devices]
|
|
||||||
for (const target of allTargets) {
|
|
||||||
onSubmit(target, command)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getDialogProps = () => {
|
|
||||||
if (dialogType === "room") {
|
|
||||||
return {
|
|
||||||
title: "Chọn phòng để gửi lệnh",
|
|
||||||
description: "Chọn các phòng muốn gửi lệnh CMD tới",
|
|
||||||
icon: <Building2 className="w-6 h-6 text-primary" />,
|
|
||||||
items: rooms,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (dialogType === "device") {
|
|
||||||
return {
|
|
||||||
title: "Chọn thiết bị để gửi lệnh",
|
|
||||||
description: "Chọn các thiết bị muốn gửi lệnh CMD tới",
|
|
||||||
icon: <Monitor className="w-6 h-6 text-primary" />,
|
|
||||||
items: devices,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const dialogProps = getDialogProps()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full px-6 space-y-4">
|
<div className="w-full px-6 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -97,35 +58,33 @@ export function FormSubmitTemplate({
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>Nhập và gửi lệnh xuống thiết bị</CardDescription>
|
<CardDescription>Nhập và gửi lệnh xuống thiết bị</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
<CardContent>{children({ command, setCommand })}</CardContent>
|
{children({ command, setCommand })}
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
{onSubmit && (
|
{onSubmit && (
|
||||||
<CardFooter className="flex justify-end">
|
<CardFooter className="flex gap-2">
|
||||||
<RequestUpdateMenu
|
<UpdateButton
|
||||||
onUpdateDevice={openDeviceDialog}
|
onClick={handleClick}
|
||||||
onUpdateRoom={openRoomDialog}
|
|
||||||
onUpdateAll={handleSubmitAll}
|
|
||||||
loading={submitLoading}
|
loading={submitLoading}
|
||||||
|
label="Yêu cầu theo phòng"
|
||||||
|
/>
|
||||||
|
<UpdateButton
|
||||||
|
onClick={() => onSubmit("All", command)}
|
||||||
|
loading={submitLoading}
|
||||||
|
label="Cập nhật tất cả thiết bị"
|
||||||
/>
|
/>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 🧩 Dùng SelectDialog chung */}
|
{onSubmit && rooms.length > 0 && (
|
||||||
{dialogProps && (
|
<RoomSelectDialog
|
||||||
<SelectDialog
|
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
onClose={() => setDialogOpen(false)}
|
onClose={() => setDialogOpen(false)}
|
||||||
title={dialogProps.title}
|
rooms={rooms}
|
||||||
description={dialogProps.description}
|
onConfirm={(roomName) => {
|
||||||
icon={dialogProps.icon}
|
onSubmit(roomName, command)
|
||||||
items={dialogProps.items}
|
|
||||||
onConfirm={async (selectedItems) => {
|
|
||||||
if (!onSubmit) return
|
|
||||||
for (const item of selectedItems) {
|
|
||||||
await onSubmit(item, command)
|
|
||||||
}
|
|
||||||
setDialogOpen(false)
|
setDialogOpen(false)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,153 +0,0 @@
|
||||||
import { RequestUpdateMenu } from "@/components/request-update-menu";
|
|
||||||
import { SelectDialog } from "@/components/select-dialog";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { UploadDialog } from "@/components/upload-dialog";
|
|
||||||
import { VersionTable } from "@/components/version-table";
|
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
|
||||||
import type { AxiosProgressEvent } from "axios";
|
|
||||||
import { FileText, Building2, Monitor } from "lucide-react";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
interface BlackListManagerTemplateProps<TData> {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
data: TData[];
|
|
||||||
isLoading: boolean;
|
|
||||||
columns: ColumnDef<TData, any>[];
|
|
||||||
onUpload: (
|
|
||||||
fd: FormData,
|
|
||||||
config?: { onUploadProgress?: (e: AxiosProgressEvent) => void }
|
|
||||||
) => Promise<void>;
|
|
||||||
onUpdate?: (roomName: string) => void;
|
|
||||||
updateLoading?: boolean;
|
|
||||||
onTableInit?: (table: any) => void;
|
|
||||||
rooms: string[];
|
|
||||||
devices?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BlackListManagerTemplate<TData>({
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
data,
|
|
||||||
isLoading,
|
|
||||||
columns,
|
|
||||||
onUpload,
|
|
||||||
onUpdate,
|
|
||||||
updateLoading,
|
|
||||||
onTableInit,
|
|
||||||
rooms = [],
|
|
||||||
devices = [],
|
|
||||||
}: BlackListManagerTemplateProps<TData>) {
|
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
|
||||||
const [dialogType, setDialogType] = useState<"room" | "device" | null>(null);
|
|
||||||
|
|
||||||
const handleUpdateAll = () => {
|
|
||||||
if (onUpdate) onUpdate("All");
|
|
||||||
};
|
|
||||||
|
|
||||||
const openRoomDialog = () => {
|
|
||||||
if (rooms.length > 0 && onUpdate) {
|
|
||||||
setDialogType("room");
|
|
||||||
setDialogOpen(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const openDeviceDialog = () => {
|
|
||||||
if (devices.length > 0 && onUpdate) {
|
|
||||||
setDialogType("device");
|
|
||||||
setDialogOpen(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDialogProps = () => {
|
|
||||||
if (dialogType === "room") {
|
|
||||||
return {
|
|
||||||
title: "Chọn phòng",
|
|
||||||
description: "Chọn các phòng cần cập nhật",
|
|
||||||
icon: <Building2 className="w-6 h-6 text-primary" />,
|
|
||||||
items: rooms,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (dialogType === "device") {
|
|
||||||
return {
|
|
||||||
title: "Chọn thiết bị",
|
|
||||||
description: "Chọn các thiết bị cần cập nhật",
|
|
||||||
icon: <Monitor className="w-6 h-6 text-primary" />,
|
|
||||||
items: devices,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
const dialogProps = getDialogProps();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full px-6 space-y-4">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold">{title}</h1>
|
|
||||||
<p className="text-muted-foreground mt-2">{description}</p>
|
|
||||||
</div>
|
|
||||||
<UploadDialog onSubmit={onUpload} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Table */}
|
|
||||||
<Card className="w-full">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<FileText className="h-5 w-5" /> Danh sách phần mềm bị chặn
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Các phần mềm không được cho phép trong hệ thống
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent>
|
|
||||||
<VersionTable
|
|
||||||
data={data}
|
|
||||||
isLoading={isLoading}
|
|
||||||
columns={columns}
|
|
||||||
onTableInit={onTableInit}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
{onUpdate && (
|
|
||||||
<CardFooter className="flex flex-col sm:flex-row gap-2">
|
|
||||||
<RequestUpdateMenu
|
|
||||||
onUpdateDevice={openDeviceDialog}
|
|
||||||
onUpdateRoom={openRoomDialog}
|
|
||||||
onUpdateAll={handleUpdateAll}
|
|
||||||
loading={updateLoading}
|
|
||||||
/>
|
|
||||||
</CardFooter>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{dialogProps && (
|
|
||||||
<SelectDialog
|
|
||||||
open={dialogOpen}
|
|
||||||
onClose={() => setDialogOpen(false)}
|
|
||||||
title={dialogProps.title}
|
|
||||||
description={dialogProps.description}
|
|
||||||
icon={dialogProps.icon}
|
|
||||||
items={dialogProps.items}
|
|
||||||
onConfirm={async (selectedItems) => {
|
|
||||||
if (!onUpdate) return;
|
|
||||||
for (const item of selectedItems) {
|
|
||||||
onUpdate(item);
|
|
||||||
}
|
|
||||||
setDialogOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
export interface NetworkInfo {
|
|
||||||
macAddress?: string;
|
|
||||||
ipAddress?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DeviceHealthCheck {
|
|
||||||
id: string;
|
|
||||||
deviceTime: string;
|
|
||||||
version?: string;
|
|
||||||
room?: string;
|
|
||||||
isOffline: boolean;
|
|
||||||
networkInfos: NetworkInfo[];
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user