Add role and permission config
This commit is contained in:
parent
beb79025b2
commit
5e29ac78f7
8
.idea/TTMT.ManageWebGUI.iml
Normal file
8
.idea/TTMT.ManageWebGUI.iml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/TTMT.ManageWebGUI.iml" filepath="$PROJECT_DIR$/.idea/TTMT.ManageWebGUI.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
74
.idea/workspace.xml
Normal file
74
.idea/workspace.xml
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AutoImportSettings">
|
||||||
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
|
</component>
|
||||||
|
<component name="ChangeListManager">
|
||||||
|
<list default="true" id="94f75d5b-3c15-4d7e-9da7-ad6f5328faf8" name="Changes" comment="">
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/hooks/useAuth.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/hooks/useAuth.tsx" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/routeTree.gen.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/routeTree.gen.ts" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/routes/_auth/blacklist/index.tsx" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/routes/_auth/command/index.tsx" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/routes/_auth/room/$roomName/index.tsx" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/routes/_auth/room/index.tsx" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/types/app-sidebar.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/types/app-sidebar.ts" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/types/permission.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/types/permission.ts" afterDir="false" />
|
||||||
|
</list>
|
||||||
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
|
</component>
|
||||||
|
<component name="Git.Settings">
|
||||||
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectColorInfo"><![CDATA[{
|
||||||
|
"associatedIndex": 1
|
||||||
|
}]]></component>
|
||||||
|
<component name="ProjectId" id="3AQVfIkiaizPRlnpMICDG3COfJV" />
|
||||||
|
<component name="ProjectViewState">
|
||||||
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
|
<option name="showLibraryContents" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PropertiesComponent"><![CDATA[{
|
||||||
|
"keyToString": {
|
||||||
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||||
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||||
|
"dart.analysis.tool.window.visible": "false",
|
||||||
|
"git-widget-placeholder": "main",
|
||||||
|
"ignore.virus.scanning.warn.message": "true",
|
||||||
|
"javascript.preferred.runtime.type.id": "node",
|
||||||
|
"node.js.detected.package.eslint": "true",
|
||||||
|
"node.js.detected.package.tslint": "true",
|
||||||
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
|
"nodejs_package_manager_path": "npm",
|
||||||
|
"ts.external.directory.path": "D:\\MyProject\\NAVISProject\\TTMT.ManageWebGUI\\node_modules\\typescript\\lib",
|
||||||
|
"vue.rearranger.settings.migration": "true"
|
||||||
|
}
|
||||||
|
}]]></component>
|
||||||
|
<component name="SharedIndexes">
|
||||||
|
<attachedChunks>
|
||||||
|
<set>
|
||||||
|
<option value="bundled-js-predefined-d6986cc7102b-9b0f141eb926-JavaScript-WS-253.31033.133" />
|
||||||
|
</set>
|
||||||
|
</attachedChunks>
|
||||||
|
</component>
|
||||||
|
<component name="TaskManager">
|
||||||
|
<task active="true" id="Default" summary="Default task">
|
||||||
|
<changelist id="94f75d5b-3c15-4d7e-9da7-ad6f5328faf8" name="Changes" comment="" />
|
||||||
|
<created>1772524885874</created>
|
||||||
|
<option name="number" value="Default" />
|
||||||
|
<option name="presentableId" value="Default" />
|
||||||
|
<updated>1772524885874</updated>
|
||||||
|
<workItem from="1772524887267" duration="1839000" />
|
||||||
|
</task>
|
||||||
|
<servers />
|
||||||
|
</component>
|
||||||
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
|
<option name="version" value="3" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
26
debug-permissions.html
Normal file
26
debug-permissions.html
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Debug Permissions</title>
|
||||||
|
<script>
|
||||||
|
// Paste this in browser console to debug
|
||||||
|
const acs = localStorage.getItem('acs');
|
||||||
|
const role = localStorage.getItem('role');
|
||||||
|
console.log('Current Role:', role);
|
||||||
|
console.log('Current ACS (raw):', acs);
|
||||||
|
console.log('Current ACS (parsed):', acs ? acs.split(',').map(Number) : []);
|
||||||
|
console.log('VIEW_AGENT permission code:', 171);
|
||||||
|
console.log('Has VIEW_AGENT?', acs ? acs.split(',').map(Number).includes(171) : false);
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Open Browser Console (F12)</h1>
|
||||||
|
<p>Or run this in console:</p>
|
||||||
|
<pre>
|
||||||
|
const acs = localStorage.getItem('acs');
|
||||||
|
console.log('Your permissions:', acs);
|
||||||
|
console.log('As array:', acs ? acs.split(',').map(Number) : []);
|
||||||
|
console.log('Has VIEW_AGENT (171)?', acs ? acs.split(',').includes('171') : false);
|
||||||
|
</pre>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1754
package-lock.json
generated
1754
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -34,8 +34,10 @@
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.525.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"radix-ui": "^1.4.3",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"shadcn": "^2.9.3",
|
"shadcn": "^2.9.3",
|
||||||
|
|
|
||||||
37
src/components/app-breadcrumb.tsx
Normal file
37
src/components/app-breadcrumb.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator
|
||||||
|
} from "./ui/breadcrumb";
|
||||||
|
import { Link, useMatches } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export default function AppBreadCrumb() {
|
||||||
|
const matches = useMatches();
|
||||||
|
|
||||||
|
const crumbs = matches
|
||||||
|
.filter((m) => Boolean(m.context.breadcrumbs))
|
||||||
|
.map((m) => m.context.breadcrumbs)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const displayCrumbs = crumbs[0] as { path: string; title: string }[];
|
||||||
|
if (displayCrumbs == null || displayCrumbs.length == 0) return;
|
||||||
|
return (
|
||||||
|
<Breadcrumb className="flex-1">
|
||||||
|
<BreadcrumbList>
|
||||||
|
{displayCrumbs.slice(0, -1).map((b, index) => (
|
||||||
|
<>
|
||||||
|
<BreadcrumbItem key={index} className="md:block">
|
||||||
|
<Link to={b.path}>{b.title}</Link>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
<BreadcrumbSeparator className="hidden md:block" />
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
<BreadcrumbItem className="md:block">
|
||||||
|
<BreadcrumbPage>{displayCrumbs[displayCrumbs.length - 1].title}</BreadcrumbPage>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</BreadcrumbList>
|
||||||
|
</Breadcrumb>
|
||||||
|
);
|
||||||
|
}
|
||||||
85
src/components/avatar-dropdown.tsx
Normal file
85
src/components/avatar-dropdown.tsx
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
|
import { LogOut, Settings, User } from "lucide-react";
|
||||||
|
|
||||||
|
interface AvatarDropdownProps {
|
||||||
|
username: string;
|
||||||
|
role: {
|
||||||
|
roleName: string;
|
||||||
|
priority: number;
|
||||||
|
};
|
||||||
|
onLogOut: () => void;
|
||||||
|
onSettings?: () => void;
|
||||||
|
onProfile?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AvatarDropdown({
|
||||||
|
username,
|
||||||
|
role,
|
||||||
|
onLogOut,
|
||||||
|
onSettings,
|
||||||
|
onProfile,
|
||||||
|
}: AvatarDropdownProps) {
|
||||||
|
// Get initials from username
|
||||||
|
const getInitials = (name: string): string => {
|
||||||
|
if (!name) return "U";
|
||||||
|
const parts = name.split(" ");
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
return `${parts[0][0]}${parts[1][0]}`.toUpperCase();
|
||||||
|
}
|
||||||
|
return name.substring(0, 2).toUpperCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button className="flex items-center gap-2 rounded-full focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2">
|
||||||
|
<Avatar className="h-9 w-9 cursor-pointer">
|
||||||
|
<AvatarImage src="" alt={username} />
|
||||||
|
<AvatarFallback className="bg-primary text-primary-foreground text-sm font-medium">
|
||||||
|
{getInitials(username)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-56">
|
||||||
|
<DropdownMenuLabel className="font-normal">
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<p className="text-sm font-medium leading-none">{username}</p>
|
||||||
|
<p className="text-xs leading-none text-muted-foreground">
|
||||||
|
{role.roleName || "Người dùng"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
{onProfile && (
|
||||||
|
<DropdownMenuItem onClick={onProfile} className="cursor-pointer">
|
||||||
|
<User className="mr-2 h-4 w-4" />
|
||||||
|
<span>Tài khoản của tôi</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{onSettings && (
|
||||||
|
<DropdownMenuItem onClick={onSettings} className="cursor-pointer">
|
||||||
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
|
<span>Cài đặt</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{(onProfile || onSettings) && <DropdownMenuSeparator />}
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={onLogOut}
|
||||||
|
className="cursor-pointer text-destructive focus:text-destructive"
|
||||||
|
>
|
||||||
|
<LogOut className="mr-2 h-4 w-4" />
|
||||||
|
<span>Đăng xuất</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/components/pages/error-fetching-page.tsx
Normal file
16
src/components/pages/error-fetching-page.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
|
export function ErrorFetchingPage({ error }: { error: Error | AxiosError }) {
|
||||||
|
return (
|
||||||
|
<div className="p-4 text-destructive">
|
||||||
|
<h2 className="text-xl font-semibold mb-2">Lỗi</h2>
|
||||||
|
<p>
|
||||||
|
{"isAxiosError" in error &&
|
||||||
|
error.response?.data &&
|
||||||
|
(error.response.data as { message?: string }).message
|
||||||
|
? (error.response.data as { message?: string }).message
|
||||||
|
: "Lỗi trong quá trình render page"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
src/components/pages/session-timeout-error.tsx
Normal file
43
src/components/pages/session-timeout-error.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Route } from "@/routes/_auth";
|
||||||
|
import { useRouter } from "@tanstack/react-router";
|
||||||
|
import { ArrowLeft, Search } from "lucide-react";
|
||||||
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
|
|
||||||
|
export default function SessionTimeOutErrorPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const navigate = Route.useNavigate();
|
||||||
|
const auth = useAuth();
|
||||||
|
const handleLogout = () => {
|
||||||
|
auth.logout();
|
||||||
|
router.invalidate().finally(() => {
|
||||||
|
navigate({ to: "/login" });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-[70vh] px-4 text-center">
|
||||||
|
<div className="space-y-6 max-w-md mx-auto">
|
||||||
|
<div className="relative mx-auto w-40 h-40 md:w-52 md:h-52">
|
||||||
|
<div className="absolute inset-0 bg-primary/10 rounded-full animate-pulse" />
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<Search className="h-20 w-20 md:h-24 md:w-24 text-primary" strokeWidth={1.5} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-semibold">Phiên hết hạn</h2>
|
||||||
|
<p className="text-muted-foreground">Bạn cần đăng nhập lại</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-6">
|
||||||
|
<Button asChild size="lg" className="gap-2" onClick={handleLogout}>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
Trở về trang đăng nhập
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -14,19 +14,53 @@ import {
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { appSidebarSection } from "@/types/app-sidebar";
|
||||||
|
import { PermissionEnum } from "@/types/permission";
|
||||||
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
type MenuItem = {
|
type SidebarItem = {
|
||||||
title: string;
|
title: string;
|
||||||
to: string;
|
url: string;
|
||||||
|
code?: number;
|
||||||
icon: React.ElementType;
|
icon: React.ElementType;
|
||||||
onPointerEnter?: () => void;
|
permissions?: PermissionEnum[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type AppSidebarProps = {
|
type SidebarSection = {
|
||||||
items: MenuItem[];
|
title: string;
|
||||||
|
items: SidebarItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AppSidebar({ items }: AppSidebarProps) {
|
export function AppSidebar() {
|
||||||
|
const { hasPermission, acs } = useAuth();
|
||||||
|
|
||||||
|
// Check if user is admin (has ALLOW_ALL permission)
|
||||||
|
const isAdmin = acs.includes(PermissionEnum.ALLOW_ALL);
|
||||||
|
|
||||||
|
// Check if user has any of the required permissions
|
||||||
|
const checkPermissions = (permissions?: PermissionEnum[]) => {
|
||||||
|
// No permissions defined = show to everyone
|
||||||
|
if (!permissions || permissions.length === 0) return true;
|
||||||
|
// Item marked as ALLOW_ALL = show to everyone
|
||||||
|
if (permissions.includes(PermissionEnum.ALLOW_ALL)) return true;
|
||||||
|
// Admin users see everything
|
||||||
|
if (isAdmin) return true;
|
||||||
|
// Check if user has any of the required permissions
|
||||||
|
return permissions.some((permission) => hasPermission(permission));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter sidebar sections and items based on permissions
|
||||||
|
const filteredNavMain = useMemo(() => {
|
||||||
|
return appSidebarSection.navMain
|
||||||
|
.map((section) => ({
|
||||||
|
...section,
|
||||||
|
items: section.items.filter((item) => checkPermissions(item.permissions)),
|
||||||
|
}))
|
||||||
|
.filter((section) => section.items.length > 0) as SidebarSection[];
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [acs]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar
|
<Sidebar
|
||||||
collapsible="icon"
|
collapsible="icon"
|
||||||
|
|
@ -50,18 +84,18 @@ export function AppSidebar({ items }: AppSidebarProps) {
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
|
|
||||||
<SidebarContent className="p-4">
|
<SidebarContent className="p-4">
|
||||||
<SidebarGroup>
|
{filteredNavMain.map((section) => (
|
||||||
|
<SidebarGroup key={section.title}>
|
||||||
<SidebarGroupLabel className="text-xs font-semibold text-muted-foreground/80 uppercase tracking-wider mb-2">
|
<SidebarGroupLabel className="text-xs font-semibold text-muted-foreground/80 uppercase tracking-wider mb-2">
|
||||||
Navigation
|
{section.title}
|
||||||
</SidebarGroupLabel>
|
</SidebarGroupLabel>
|
||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
<SidebarMenu className="space-y-1">
|
<SidebarMenu className="space-y-1">
|
||||||
{items.map((item) => (
|
{section.items.map((item) => (
|
||||||
<SidebarMenuItem key={item.title}>
|
<SidebarMenuItem key={item.title}>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
asChild
|
asChild
|
||||||
tooltip={item.title}
|
tooltip={item.title}
|
||||||
onPointerEnter={item.onPointerEnter}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full justify-start gap-3 px-4 py-3 rounded-xl",
|
"w-full justify-start gap-3 px-4 py-3 rounded-xl",
|
||||||
"hover:bg-accent/60 hover:text-accent-foreground hover:shadow-sm",
|
"hover:bg-accent/60 hover:text-accent-foreground hover:shadow-sm",
|
||||||
|
|
@ -72,8 +106,8 @@ export function AppSidebar({ items }: AppSidebarProps) {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href={item.to}
|
href={item.url}
|
||||||
to={"."}
|
to={item.url}
|
||||||
className="flex items-center gap-3 w-full"
|
className="flex items-center gap-3 w-full"
|
||||||
>
|
>
|
||||||
<item.icon className="size-5 shrink-0 transition-all duration-200 group-hover:scale-110 group-hover:text-primary" />
|
<item.icon className="size-5 shrink-0 transition-all duration-200 group-hover:scale-110 group-hover:text-primary" />
|
||||||
|
|
@ -87,6 +121,7 @@ export function AppSidebar({ items }: AppSidebarProps) {
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
|
))}
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
|
|
||||||
<SidebarFooter className="border-t border-border/40 p-4 space-y-3">
|
<SidebarFooter className="border-t border-border/40 p-4 space-y-3">
|
||||||
|
|
|
||||||
109
src/components/ui/breadcrumb.tsx
Normal file
109
src/components/ui/breadcrumb.tsx
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
||||||
|
import { Slot } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||||
|
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||||
|
return (
|
||||||
|
<ol
|
||||||
|
data-slot="breadcrumb-list"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-wrap items-center gap-1.5 text-sm break-words text-muted-foreground sm:gap-2.5",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
data-slot="breadcrumb-item"
|
||||||
|
className={cn("inline-flex items-center gap-1.5", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbLink({
|
||||||
|
asChild,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"a"> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot.Root : "a"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="breadcrumb-link"
|
||||||
|
className={cn("transition-colors hover:text-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="breadcrumb-page"
|
||||||
|
role="link"
|
||||||
|
aria-disabled="true"
|
||||||
|
aria-current="page"
|
||||||
|
className={cn("font-normal text-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbSeparator({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"li">) {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
data-slot="breadcrumb-separator"
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn("[&>svg]:size-3.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? <ChevronRight />}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbEllipsis({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="breadcrumb-ellipsis"
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn("flex size-9 items-center justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="size-4" />
|
||||||
|
<span className="sr-only">More</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
BreadcrumbEllipsis,
|
||||||
|
}
|
||||||
|
|
@ -47,8 +47,7 @@ export const API_ENDPOINTS = {
|
||||||
REQUEST_GET_CLIENT_FOLDER_STATUS: (roomName: string) =>
|
REQUEST_GET_CLIENT_FOLDER_STATUS: (roomName: string) =>
|
||||||
`${BASE_URL}/DeviceComm/clientfolderstatus/${roomName}`,
|
`${BASE_URL}/DeviceComm/clientfolderstatus/${roomName}`,
|
||||||
},
|
},
|
||||||
COMMAND:
|
COMMAND: {
|
||||||
{
|
|
||||||
ADD_COMMAND: `${BASE_URL}/Command/add`,
|
ADD_COMMAND: `${BASE_URL}/Command/add`,
|
||||||
GET_COMMANDS: `${BASE_URL}/Command/all`,
|
GET_COMMANDS: `${BASE_URL}/Command/all`,
|
||||||
GET_COMMAND_BY_TYPES: (types: string) => `${BASE_URL}/Command/types/${types}`,
|
GET_COMMAND_BY_TYPES: (types: string) => `${BASE_URL}/Command/types/${types}`,
|
||||||
|
|
@ -61,4 +60,49 @@ export const API_ENDPOINTS = {
|
||||||
GET_PROCESSES_LISTS: `${BASE_URL}/Sse/events/processLists`,
|
GET_PROCESSES_LISTS: `${BASE_URL}/Sse/events/processLists`,
|
||||||
GET_CLIENT_FOLDER_STATUS: `${BASE_URL}/Sse/events/clientFolderStatuses`,
|
GET_CLIENT_FOLDER_STATUS: `${BASE_URL}/Sse/events/clientFolderStatuses`,
|
||||||
},
|
},
|
||||||
|
PERMISSION: {
|
||||||
|
// Lấy danh sách permission từ enum
|
||||||
|
GET_LIST: `${BASE_URL}/Permission/list`,
|
||||||
|
|
||||||
|
// Lấy permission theo category
|
||||||
|
GET_BY_CATEGORY: `${BASE_URL}/Permission/list-by-category`,
|
||||||
|
|
||||||
|
// Lấy chi tiết permission theo value
|
||||||
|
GET_BY_VALUE: (value: number) => `${BASE_URL}/Permission/${value}`,
|
||||||
|
|
||||||
|
// Import permission từ enum vào DB (chạy 1 lần đầu tiên)
|
||||||
|
SEED_FROM_ENUM: `${BASE_URL}/Permission/seed-from-enum`,
|
||||||
|
|
||||||
|
// Lấy danh sách permission từ database
|
||||||
|
GET_DB_LIST: `${BASE_URL}/Permission/db-list`,
|
||||||
|
|
||||||
|
// Xóa permission
|
||||||
|
DELETE: (id: number) => `${BASE_URL}/Permission/${id}`,
|
||||||
|
},
|
||||||
|
ROLE: {
|
||||||
|
// Lấy danh sách tất cả roles
|
||||||
|
GET_LIST: `${BASE_URL}/Role/list`,
|
||||||
|
|
||||||
|
// Lấy chi tiết role theo ID
|
||||||
|
GET_BY_ID: (id: number) => `${BASE_URL}/Role/${id}`,
|
||||||
|
|
||||||
|
// Tạo role mới
|
||||||
|
CREATE: `${BASE_URL}/Role/create`,
|
||||||
|
|
||||||
|
// Cập nhật role
|
||||||
|
UPDATE: (id: number) => `${BASE_URL}/Role/update/${id}`,
|
||||||
|
|
||||||
|
// Xóa role
|
||||||
|
DELETE: (id: number) => `${BASE_URL}/Role/${id}`,
|
||||||
|
|
||||||
|
// Lấy danh sách permissions của role
|
||||||
|
GET_PERMISSIONS: (id: number) => `${BASE_URL}/Role/${id}/permissions`,
|
||||||
|
|
||||||
|
// Gán permissions cho role (thay thế toàn bộ)
|
||||||
|
ASSIGN_PERMISSIONS: (id: number) => `${BASE_URL}/Role/${id}/assign-permissions`,
|
||||||
|
|
||||||
|
// Bật/tắt một permission cụ thể (query param: ?isChecked=true/false)
|
||||||
|
TOGGLE_PERMISSION: (roleId: number, permissionId: number) =>
|
||||||
|
`${BASE_URL}/Role/${roleId}/permissions/${permissionId}/toggle`,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,9 @@ export * from "./useDeviceCommQueries";
|
||||||
|
|
||||||
// Command Queries
|
// Command Queries
|
||||||
export * from "./useCommandQueries";
|
export * from "./useCommandQueries";
|
||||||
|
|
||||||
|
// Permission Queries
|
||||||
|
export * from "./usePermissionQueries";
|
||||||
|
|
||||||
|
// Role Queries
|
||||||
|
export * from "./useRoleQueries";
|
||||||
|
|
|
||||||
90
src/hooks/queries/usePermissionQueries.ts
Normal file
90
src/hooks/queries/usePermissionQueries.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import * as permissionService from "@/services/permission.service";
|
||||||
|
|
||||||
|
export const PERMISSION_QUERY_KEYS = {
|
||||||
|
all: ["permissions"] as const,
|
||||||
|
list: () => [...PERMISSION_QUERY_KEYS.all, "list"] as const,
|
||||||
|
dbList: () => [...PERMISSION_QUERY_KEYS.all, "db-list"] as const,
|
||||||
|
byCategory: () => [...PERMISSION_QUERY_KEYS.all, "by-category"] as const,
|
||||||
|
detail: (value: number) => [...PERMISSION_QUERY_KEYS.all, "detail", value] as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook để lấy danh sách permission từ enum
|
||||||
|
*/
|
||||||
|
export function useGetPermissionList(enabled = true) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: PERMISSION_QUERY_KEYS.list(),
|
||||||
|
queryFn: () => permissionService.getPermissionList(),
|
||||||
|
enabled,
|
||||||
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook để lấy permission theo category
|
||||||
|
*/
|
||||||
|
export function useGetPermissionByCategory(enabled = true) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: PERMISSION_QUERY_KEYS.byCategory(),
|
||||||
|
queryFn: () => permissionService.getPermissionByCategory(),
|
||||||
|
enabled,
|
||||||
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook để lấy chi tiết permission theo value
|
||||||
|
*/
|
||||||
|
export function useGetPermissionByValue(value: number, enabled = true) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: PERMISSION_QUERY_KEYS.detail(value),
|
||||||
|
queryFn: () => permissionService.getPermissionByValue(value),
|
||||||
|
enabled: enabled && value > 0,
|
||||||
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook để lấy danh sách permission từ database
|
||||||
|
*/
|
||||||
|
export function useGetPermissionDbList(enabled = true) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: PERMISSION_QUERY_KEYS.dbList(),
|
||||||
|
queryFn: () => permissionService.getPermissionDbList(),
|
||||||
|
enabled,
|
||||||
|
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook để seed permission từ enum vào DB
|
||||||
|
*/
|
||||||
|
export function useSeedPermissionFromEnum() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () => permissionService.seedPermissionFromEnum(),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: PERMISSION_QUERY_KEYS.all,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook để xóa permission
|
||||||
|
*/
|
||||||
|
export function useDeletePermission() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: number) => permissionService.deletePermission(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: PERMISSION_QUERY_KEYS.all,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
149
src/hooks/queries/useRoleQueries.ts
Normal file
149
src/hooks/queries/useRoleQueries.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import * as roleService from "@/services/role.service";
|
||||||
|
import type { TCreateRoleRequestBody } from "@/types/role";
|
||||||
|
|
||||||
|
export const ROLE_QUERY_KEYS = {
|
||||||
|
all: ["roles"] as const,
|
||||||
|
list: () => [...ROLE_QUERY_KEYS.all, "list"] as const,
|
||||||
|
detail: (id: number) => [...ROLE_QUERY_KEYS.all, "detail", id] as const,
|
||||||
|
permissions: (id: number) => [...ROLE_QUERY_KEYS.all, "permissions", id] as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook để lấy danh sách roles
|
||||||
|
*/
|
||||||
|
export function useGetRoleList(enabled = true) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ROLE_QUERY_KEYS.list(),
|
||||||
|
queryFn: () => roleService.getRoleList(),
|
||||||
|
enabled,
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook để lấy chi tiết role theo ID
|
||||||
|
*/
|
||||||
|
export function useGetRoleById(id: number, enabled = true) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ROLE_QUERY_KEYS.detail(id),
|
||||||
|
queryFn: () => roleService.getRoleById(id),
|
||||||
|
enabled: enabled && id > 0,
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook để lấy danh sách permissions của role
|
||||||
|
*/
|
||||||
|
export function useGetRolePermissions(id: number, enabled = true) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ROLE_QUERY_KEYS.permissions(id),
|
||||||
|
queryFn: () => roleService.getRolePermissions(id),
|
||||||
|
enabled: enabled && id > 0,
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook để tạo role mới
|
||||||
|
*/
|
||||||
|
export function useCreateRole() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data: TCreateRoleRequestBody) => roleService.createRole(data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ROLE_QUERY_KEYS.list(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook để cập nhật role
|
||||||
|
*/
|
||||||
|
export function useUpdateRole() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
id: number;
|
||||||
|
data: Partial<TCreateRoleRequestBody>;
|
||||||
|
}) => roleService.updateRole(id, data),
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ROLE_QUERY_KEYS.list(),
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ROLE_QUERY_KEYS.detail(variables.id),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook để xóa role
|
||||||
|
*/
|
||||||
|
export function useDeleteRole() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: number) => roleService.deleteRole(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ROLE_QUERY_KEYS.list(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook để gán permissions cho role
|
||||||
|
*/
|
||||||
|
export function useAssignRolePermissions() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
roleId,
|
||||||
|
permissionIds,
|
||||||
|
}: {
|
||||||
|
roleId: number;
|
||||||
|
permissionIds: number[];
|
||||||
|
}) => roleService.assignRolePermissions(roleId, permissionIds),
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ROLE_QUERY_KEYS.permissions(variables.roleId),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook để bật/tắt một permission của role
|
||||||
|
*/
|
||||||
|
export function useToggleRolePermission() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
roleId,
|
||||||
|
permissionId,
|
||||||
|
isChecked,
|
||||||
|
}: {
|
||||||
|
roleId: number;
|
||||||
|
permissionId: number;
|
||||||
|
isChecked: boolean;
|
||||||
|
}) => roleService.toggleRolePermission(roleId, permissionId, isChecked),
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ROLE_QUERY_KEYS.permissions(variables.roleId),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -21,7 +21,7 @@ export interface IAuthContext {
|
||||||
|
|
||||||
const AuthContext = React.createContext<IAuthContext | null>(null);
|
const AuthContext = React.createContext<IAuthContext | null>(null);
|
||||||
|
|
||||||
const key = "accesscontrol.auth.user";
|
const key = "computersmanagement.auth.user";
|
||||||
|
|
||||||
function getStoredUser() {
|
function getStoredUser() {
|
||||||
return localStorage.getItem(key);
|
return localStorage.getItem(key);
|
||||||
|
|
|
||||||
|
|
@ -5,89 +5,19 @@ import {
|
||||||
SidebarInset,
|
SidebarInset,
|
||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { Home, Building, AppWindow, Terminal, CircleX } from "lucide-react";
|
import { Building } from "lucide-react";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import {
|
|
||||||
useGetAgentVersion,
|
|
||||||
useGetSoftwareList,
|
|
||||||
useGetRoomList,
|
|
||||||
useGetBlacklist,
|
|
||||||
} from "@/hooks/queries";
|
|
||||||
|
|
||||||
type AppLayoutProps = {
|
type AppLayoutProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AppLayout({ children }: AppLayoutProps) {
|
export default function AppLayout({ children }: AppLayoutProps) {
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const handlePrefetchAgents = () => {
|
|
||||||
queryClient.prefetchQuery({
|
|
||||||
queryKey: ["app-version", "agent"],
|
|
||||||
queryFn: useGetAgentVersion as any,
|
|
||||||
staleTime: 60 * 1000,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePrefetchSofware = () => {
|
|
||||||
queryClient.prefetchQuery({
|
|
||||||
queryKey: ["app-version", "software"],
|
|
||||||
queryFn: useGetSoftwareList as any,
|
|
||||||
staleTime: 60 * 1000,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePrefetchRooms = () => {
|
|
||||||
queryClient.prefetchQuery({
|
|
||||||
queryKey: ["device-comm", "rooms"],
|
|
||||||
queryFn: useGetRoomList as any,
|
|
||||||
staleTime: 5 * 60 * 1000,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePrefetchBannedSoftware = () => {
|
|
||||||
queryClient.prefetchQuery({
|
|
||||||
queryKey: ["app-version", "blacklist"],
|
|
||||||
queryFn: useGetBlacklist as any,
|
|
||||||
staleTime: 60 * 1000,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const items = [
|
|
||||||
{ title: "Dashboard", to: "/", icon: Home },
|
|
||||||
{
|
|
||||||
title: "Danh sách phòng",
|
|
||||||
to: "/room",
|
|
||||||
icon: Building,
|
|
||||||
onPointerEnter: handlePrefetchRooms,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Quản lý Agent",
|
|
||||||
to: "/agent",
|
|
||||||
icon: AppWindow,
|
|
||||||
onPointerEnter: handlePrefetchAgents,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Quản lý phần mềm",
|
|
||||||
to: "/apps",
|
|
||||||
icon: AppWindow,
|
|
||||||
onPointerEnter: handlePrefetchSofware,
|
|
||||||
},
|
|
||||||
{ title: "Gửi lệnh CMD", to: "/command", icon: Terminal },
|
|
||||||
{
|
|
||||||
title: "Danh sách đen",
|
|
||||||
to: "/blacklist",
|
|
||||||
icon: CircleX,
|
|
||||||
onPointerEnter: handlePrefetchBannedSoftware,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<div className="flex min-h-screen w-full bg-background">
|
<div className="flex min-h-screen w-full bg-background">
|
||||||
<AppSidebar items={items} />
|
<AppSidebar />
|
||||||
<SidebarInset className="flex-1">
|
<SidebarInset className="flex-1">
|
||||||
<header className="flex h-16 shrink-0 items-center gap-2 border-b border-border/40 bg-background/95 backdrop-blur px-4 lg:hidden supports-[backdrop-filter]:bg-background/60">
|
<header className="flex h-16 shrink-0 items-center gap-2 border-b border-border/40 bg-background/95 backdrop-blur px-4 lg:hidden supports-[backdrop-filter]:bg-background/60">
|
||||||
<SidebarTrigger className="-ml-1 hover:bg-accent/50 rounded-lg p-2 transition-colors" />
|
<SidebarTrigger className="-ml-1 hover:bg-accent/50 rounded-lg p-2 transition-colors" />
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { clsx, type ClassValue } from "clsx"
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { format } from "date-fns";
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
|
@ -8,3 +9,27 @@ export function cn(...inputs: ClassValue[]) {
|
||||||
export function sleep(ms: number): Promise<void> {
|
export function sleep(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_DATE_FORMAT = "dd/MM/yyyy";
|
||||||
|
|
||||||
|
|
||||||
|
export function getCurrentTimeUTC(): string {
|
||||||
|
const date = new Date();
|
||||||
|
const month = (date.getUTCMonth() + 1).toString().padStart(2, "0");
|
||||||
|
const day = date.getUTCDate().toString().padStart(2, "0");
|
||||||
|
const year = date.getUTCFullYear();
|
||||||
|
const hour = date.getUTCHours().toString().padStart(2, "0");
|
||||||
|
const min = date.getUTCMinutes().toString().padStart(2, "0");
|
||||||
|
|
||||||
|
return `${day}/${month}/${year} ${hour}:${min}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDate(
|
||||||
|
date: string | Date | undefined | null,
|
||||||
|
formatTemplate = DEFAULT_DATE_FORMAT
|
||||||
|
) {
|
||||||
|
if (date == undefined) return "";
|
||||||
|
if (date == null) return "";
|
||||||
|
return format(new Date(date), formatTemplate, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,15 +11,18 @@
|
||||||
import { Route as rootRouteImport } from './routes/__root'
|
import { Route as rootRouteImport } from './routes/__root'
|
||||||
import { Route as AuthRouteImport } from './routes/_auth'
|
import { Route as AuthRouteImport } from './routes/_auth'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
import { Route as AuthRoomIndexRouteImport } from './routes/_auth/room/index'
|
import { Route as AuthRoomsIndexRouteImport } from './routes/_auth/rooms/index'
|
||||||
|
import { Route as AuthRoleIndexRouteImport } from './routes/_auth/role/index'
|
||||||
import { Route as AuthDeviceIndexRouteImport } from './routes/_auth/device/index'
|
import { Route as AuthDeviceIndexRouteImport } from './routes/_auth/device/index'
|
||||||
import { Route as AuthDashboardIndexRouteImport } from './routes/_auth/dashboard/index'
|
import { Route as AuthDashboardIndexRouteImport } from './routes/_auth/dashboard/index'
|
||||||
import { Route as AuthCommandIndexRouteImport } from './routes/_auth/command/index'
|
import { Route as AuthCommandsIndexRouteImport } from './routes/_auth/commands/index'
|
||||||
import { Route as AuthBlacklistIndexRouteImport } from './routes/_auth/blacklist/index'
|
import { Route as AuthBlacklistsIndexRouteImport } from './routes/_auth/blacklists/index'
|
||||||
import { Route as AuthAppsIndexRouteImport } from './routes/_auth/apps/index'
|
import { Route as AuthAppsIndexRouteImport } from './routes/_auth/apps/index'
|
||||||
import { Route as AuthAgentIndexRouteImport } from './routes/_auth/agent/index'
|
import { Route as AuthAgentIndexRouteImport } from './routes/_auth/agent/index'
|
||||||
import { Route as authLoginIndexRouteImport } from './routes/(auth)/login/index'
|
import { Route as authLoginIndexRouteImport } from './routes/(auth)/login/index'
|
||||||
import { Route as AuthRoomRoomNameIndexRouteImport } from './routes/_auth/room/$roomName/index'
|
import { Route as AuthRoomsRoomNameIndexRouteImport } from './routes/_auth/rooms/$roomName/index'
|
||||||
|
import { Route as AuthRoleCreateIndexRouteImport } from './routes/_auth/role/create/index'
|
||||||
|
import { Route as AuthRoleIdEditIndexRouteImport } from './routes/_auth/role/$id/edit/index'
|
||||||
|
|
||||||
const AuthRoute = AuthRouteImport.update({
|
const AuthRoute = AuthRouteImport.update({
|
||||||
id: '/_auth',
|
id: '/_auth',
|
||||||
|
|
@ -30,9 +33,14 @@ const IndexRoute = IndexRouteImport.update({
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
const AuthRoomIndexRoute = AuthRoomIndexRouteImport.update({
|
const AuthRoomsIndexRoute = AuthRoomsIndexRouteImport.update({
|
||||||
id: '/room/',
|
id: '/rooms/',
|
||||||
path: '/room/',
|
path: '/rooms/',
|
||||||
|
getParentRoute: () => AuthRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthRoleIndexRoute = AuthRoleIndexRouteImport.update({
|
||||||
|
id: '/role/',
|
||||||
|
path: '/role/',
|
||||||
getParentRoute: () => AuthRoute,
|
getParentRoute: () => AuthRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const AuthDeviceIndexRoute = AuthDeviceIndexRouteImport.update({
|
const AuthDeviceIndexRoute = AuthDeviceIndexRouteImport.update({
|
||||||
|
|
@ -45,14 +53,14 @@ const AuthDashboardIndexRoute = AuthDashboardIndexRouteImport.update({
|
||||||
path: '/dashboard/',
|
path: '/dashboard/',
|
||||||
getParentRoute: () => AuthRoute,
|
getParentRoute: () => AuthRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const AuthCommandIndexRoute = AuthCommandIndexRouteImport.update({
|
const AuthCommandsIndexRoute = AuthCommandsIndexRouteImport.update({
|
||||||
id: '/command/',
|
id: '/commands/',
|
||||||
path: '/command/',
|
path: '/commands/',
|
||||||
getParentRoute: () => AuthRoute,
|
getParentRoute: () => AuthRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const AuthBlacklistIndexRoute = AuthBlacklistIndexRouteImport.update({
|
const AuthBlacklistsIndexRoute = AuthBlacklistsIndexRouteImport.update({
|
||||||
id: '/blacklist/',
|
id: '/blacklists/',
|
||||||
path: '/blacklist/',
|
path: '/blacklists/',
|
||||||
getParentRoute: () => AuthRoute,
|
getParentRoute: () => AuthRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const AuthAppsIndexRoute = AuthAppsIndexRouteImport.update({
|
const AuthAppsIndexRoute = AuthAppsIndexRouteImport.update({
|
||||||
|
|
@ -70,9 +78,19 @@ const authLoginIndexRoute = authLoginIndexRouteImport.update({
|
||||||
path: '/login/',
|
path: '/login/',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
const AuthRoomRoomNameIndexRoute = AuthRoomRoomNameIndexRouteImport.update({
|
const AuthRoomsRoomNameIndexRoute = AuthRoomsRoomNameIndexRouteImport.update({
|
||||||
id: '/room/$roomName/',
|
id: '/rooms/$roomName/',
|
||||||
path: '/room/$roomName/',
|
path: '/rooms/$roomName/',
|
||||||
|
getParentRoute: () => AuthRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthRoleCreateIndexRoute = AuthRoleCreateIndexRouteImport.update({
|
||||||
|
id: '/role/create/',
|
||||||
|
path: '/role/create/',
|
||||||
|
getParentRoute: () => AuthRoute,
|
||||||
|
} as any)
|
||||||
|
const AuthRoleIdEditIndexRoute = AuthRoleIdEditIndexRouteImport.update({
|
||||||
|
id: '/role/$id/edit/',
|
||||||
|
path: '/role/$id/edit/',
|
||||||
getParentRoute: () => AuthRoute,
|
getParentRoute: () => AuthRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
|
@ -81,24 +99,30 @@ export interface FileRoutesByFullPath {
|
||||||
'/login': typeof authLoginIndexRoute
|
'/login': typeof authLoginIndexRoute
|
||||||
'/agent': typeof AuthAgentIndexRoute
|
'/agent': typeof AuthAgentIndexRoute
|
||||||
'/apps': typeof AuthAppsIndexRoute
|
'/apps': typeof AuthAppsIndexRoute
|
||||||
'/blacklist': typeof AuthBlacklistIndexRoute
|
'/blacklists': typeof AuthBlacklistsIndexRoute
|
||||||
'/command': typeof AuthCommandIndexRoute
|
'/commands': typeof AuthCommandsIndexRoute
|
||||||
'/dashboard': typeof AuthDashboardIndexRoute
|
'/dashboard': typeof AuthDashboardIndexRoute
|
||||||
'/device': typeof AuthDeviceIndexRoute
|
'/device': typeof AuthDeviceIndexRoute
|
||||||
'/room': typeof AuthRoomIndexRoute
|
'/role': typeof AuthRoleIndexRoute
|
||||||
'/room/$roomName': typeof AuthRoomRoomNameIndexRoute
|
'/rooms': typeof AuthRoomsIndexRoute
|
||||||
|
'/role/create': typeof AuthRoleCreateIndexRoute
|
||||||
|
'/rooms/$roomName': typeof AuthRoomsRoomNameIndexRoute
|
||||||
|
'/role/$id/edit': typeof AuthRoleIdEditIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/login': typeof authLoginIndexRoute
|
'/login': typeof authLoginIndexRoute
|
||||||
'/agent': typeof AuthAgentIndexRoute
|
'/agent': typeof AuthAgentIndexRoute
|
||||||
'/apps': typeof AuthAppsIndexRoute
|
'/apps': typeof AuthAppsIndexRoute
|
||||||
'/blacklist': typeof AuthBlacklistIndexRoute
|
'/blacklists': typeof AuthBlacklistsIndexRoute
|
||||||
'/command': typeof AuthCommandIndexRoute
|
'/commands': typeof AuthCommandsIndexRoute
|
||||||
'/dashboard': typeof AuthDashboardIndexRoute
|
'/dashboard': typeof AuthDashboardIndexRoute
|
||||||
'/device': typeof AuthDeviceIndexRoute
|
'/device': typeof AuthDeviceIndexRoute
|
||||||
'/room': typeof AuthRoomIndexRoute
|
'/role': typeof AuthRoleIndexRoute
|
||||||
'/room/$roomName': typeof AuthRoomRoomNameIndexRoute
|
'/rooms': typeof AuthRoomsIndexRoute
|
||||||
|
'/role/create': typeof AuthRoleCreateIndexRoute
|
||||||
|
'/rooms/$roomName': typeof AuthRoomsRoomNameIndexRoute
|
||||||
|
'/role/$id/edit': typeof AuthRoleIdEditIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
|
|
@ -107,12 +131,15 @@ export interface FileRoutesById {
|
||||||
'/(auth)/login/': typeof authLoginIndexRoute
|
'/(auth)/login/': typeof authLoginIndexRoute
|
||||||
'/_auth/agent/': typeof AuthAgentIndexRoute
|
'/_auth/agent/': typeof AuthAgentIndexRoute
|
||||||
'/_auth/apps/': typeof AuthAppsIndexRoute
|
'/_auth/apps/': typeof AuthAppsIndexRoute
|
||||||
'/_auth/blacklist/': typeof AuthBlacklistIndexRoute
|
'/_auth/blacklists/': typeof AuthBlacklistsIndexRoute
|
||||||
'/_auth/command/': typeof AuthCommandIndexRoute
|
'/_auth/commands/': typeof AuthCommandsIndexRoute
|
||||||
'/_auth/dashboard/': typeof AuthDashboardIndexRoute
|
'/_auth/dashboard/': typeof AuthDashboardIndexRoute
|
||||||
'/_auth/device/': typeof AuthDeviceIndexRoute
|
'/_auth/device/': typeof AuthDeviceIndexRoute
|
||||||
'/_auth/room/': typeof AuthRoomIndexRoute
|
'/_auth/role/': typeof AuthRoleIndexRoute
|
||||||
'/_auth/room/$roomName/': typeof AuthRoomRoomNameIndexRoute
|
'/_auth/rooms/': typeof AuthRoomsIndexRoute
|
||||||
|
'/_auth/role/create/': typeof AuthRoleCreateIndexRoute
|
||||||
|
'/_auth/rooms/$roomName/': typeof AuthRoomsRoomNameIndexRoute
|
||||||
|
'/_auth/role/$id/edit/': typeof AuthRoleIdEditIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
|
|
@ -121,24 +148,30 @@ export interface FileRouteTypes {
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/agent'
|
| '/agent'
|
||||||
| '/apps'
|
| '/apps'
|
||||||
| '/blacklist'
|
| '/blacklists'
|
||||||
| '/command'
|
| '/commands'
|
||||||
| '/dashboard'
|
| '/dashboard'
|
||||||
| '/device'
|
| '/device'
|
||||||
| '/room'
|
| '/role'
|
||||||
| '/room/$roomName'
|
| '/rooms'
|
||||||
|
| '/role/create'
|
||||||
|
| '/rooms/$roomName'
|
||||||
|
| '/role/$id/edit'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/agent'
|
| '/agent'
|
||||||
| '/apps'
|
| '/apps'
|
||||||
| '/blacklist'
|
| '/blacklists'
|
||||||
| '/command'
|
| '/commands'
|
||||||
| '/dashboard'
|
| '/dashboard'
|
||||||
| '/device'
|
| '/device'
|
||||||
| '/room'
|
| '/role'
|
||||||
| '/room/$roomName'
|
| '/rooms'
|
||||||
|
| '/role/create'
|
||||||
|
| '/rooms/$roomName'
|
||||||
|
| '/role/$id/edit'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
|
|
@ -146,12 +179,15 @@ export interface FileRouteTypes {
|
||||||
| '/(auth)/login/'
|
| '/(auth)/login/'
|
||||||
| '/_auth/agent/'
|
| '/_auth/agent/'
|
||||||
| '/_auth/apps/'
|
| '/_auth/apps/'
|
||||||
| '/_auth/blacklist/'
|
| '/_auth/blacklists/'
|
||||||
| '/_auth/command/'
|
| '/_auth/commands/'
|
||||||
| '/_auth/dashboard/'
|
| '/_auth/dashboard/'
|
||||||
| '/_auth/device/'
|
| '/_auth/device/'
|
||||||
| '/_auth/room/'
|
| '/_auth/role/'
|
||||||
| '/_auth/room/$roomName/'
|
| '/_auth/rooms/'
|
||||||
|
| '/_auth/role/create/'
|
||||||
|
| '/_auth/rooms/$roomName/'
|
||||||
|
| '/_auth/role/$id/edit/'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
|
|
@ -176,11 +212,18 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof IndexRouteImport
|
preLoaderRoute: typeof IndexRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
'/_auth/room/': {
|
'/_auth/rooms/': {
|
||||||
id: '/_auth/room/'
|
id: '/_auth/rooms/'
|
||||||
path: '/room'
|
path: '/rooms'
|
||||||
fullPath: '/room'
|
fullPath: '/rooms'
|
||||||
preLoaderRoute: typeof AuthRoomIndexRouteImport
|
preLoaderRoute: typeof AuthRoomsIndexRouteImport
|
||||||
|
parentRoute: typeof AuthRoute
|
||||||
|
}
|
||||||
|
'/_auth/role/': {
|
||||||
|
id: '/_auth/role/'
|
||||||
|
path: '/role'
|
||||||
|
fullPath: '/role'
|
||||||
|
preLoaderRoute: typeof AuthRoleIndexRouteImport
|
||||||
parentRoute: typeof AuthRoute
|
parentRoute: typeof AuthRoute
|
||||||
}
|
}
|
||||||
'/_auth/device/': {
|
'/_auth/device/': {
|
||||||
|
|
@ -197,18 +240,18 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof AuthDashboardIndexRouteImport
|
preLoaderRoute: typeof AuthDashboardIndexRouteImport
|
||||||
parentRoute: typeof AuthRoute
|
parentRoute: typeof AuthRoute
|
||||||
}
|
}
|
||||||
'/_auth/command/': {
|
'/_auth/commands/': {
|
||||||
id: '/_auth/command/'
|
id: '/_auth/commands/'
|
||||||
path: '/command'
|
path: '/commands'
|
||||||
fullPath: '/command'
|
fullPath: '/commands'
|
||||||
preLoaderRoute: typeof AuthCommandIndexRouteImport
|
preLoaderRoute: typeof AuthCommandsIndexRouteImport
|
||||||
parentRoute: typeof AuthRoute
|
parentRoute: typeof AuthRoute
|
||||||
}
|
}
|
||||||
'/_auth/blacklist/': {
|
'/_auth/blacklists/': {
|
||||||
id: '/_auth/blacklist/'
|
id: '/_auth/blacklists/'
|
||||||
path: '/blacklist'
|
path: '/blacklists'
|
||||||
fullPath: '/blacklist'
|
fullPath: '/blacklists'
|
||||||
preLoaderRoute: typeof AuthBlacklistIndexRouteImport
|
preLoaderRoute: typeof AuthBlacklistsIndexRouteImport
|
||||||
parentRoute: typeof AuthRoute
|
parentRoute: typeof AuthRoute
|
||||||
}
|
}
|
||||||
'/_auth/apps/': {
|
'/_auth/apps/': {
|
||||||
|
|
@ -232,11 +275,25 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof authLoginIndexRouteImport
|
preLoaderRoute: typeof authLoginIndexRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
'/_auth/room/$roomName/': {
|
'/_auth/rooms/$roomName/': {
|
||||||
id: '/_auth/room/$roomName/'
|
id: '/_auth/rooms/$roomName/'
|
||||||
path: '/room/$roomName'
|
path: '/rooms/$roomName'
|
||||||
fullPath: '/room/$roomName'
|
fullPath: '/rooms/$roomName'
|
||||||
preLoaderRoute: typeof AuthRoomRoomNameIndexRouteImport
|
preLoaderRoute: typeof AuthRoomsRoomNameIndexRouteImport
|
||||||
|
parentRoute: typeof AuthRoute
|
||||||
|
}
|
||||||
|
'/_auth/role/create/': {
|
||||||
|
id: '/_auth/role/create/'
|
||||||
|
path: '/role/create'
|
||||||
|
fullPath: '/role/create'
|
||||||
|
preLoaderRoute: typeof AuthRoleCreateIndexRouteImport
|
||||||
|
parentRoute: typeof AuthRoute
|
||||||
|
}
|
||||||
|
'/_auth/role/$id/edit/': {
|
||||||
|
id: '/_auth/role/$id/edit/'
|
||||||
|
path: '/role/$id/edit'
|
||||||
|
fullPath: '/role/$id/edit'
|
||||||
|
preLoaderRoute: typeof AuthRoleIdEditIndexRouteImport
|
||||||
parentRoute: typeof AuthRoute
|
parentRoute: typeof AuthRoute
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -245,23 +302,29 @@ declare module '@tanstack/react-router' {
|
||||||
interface AuthRouteChildren {
|
interface AuthRouteChildren {
|
||||||
AuthAgentIndexRoute: typeof AuthAgentIndexRoute
|
AuthAgentIndexRoute: typeof AuthAgentIndexRoute
|
||||||
AuthAppsIndexRoute: typeof AuthAppsIndexRoute
|
AuthAppsIndexRoute: typeof AuthAppsIndexRoute
|
||||||
AuthBlacklistIndexRoute: typeof AuthBlacklistIndexRoute
|
AuthBlacklistsIndexRoute: typeof AuthBlacklistsIndexRoute
|
||||||
AuthCommandIndexRoute: typeof AuthCommandIndexRoute
|
AuthCommandsIndexRoute: typeof AuthCommandsIndexRoute
|
||||||
AuthDashboardIndexRoute: typeof AuthDashboardIndexRoute
|
AuthDashboardIndexRoute: typeof AuthDashboardIndexRoute
|
||||||
AuthDeviceIndexRoute: typeof AuthDeviceIndexRoute
|
AuthDeviceIndexRoute: typeof AuthDeviceIndexRoute
|
||||||
AuthRoomIndexRoute: typeof AuthRoomIndexRoute
|
AuthRoleIndexRoute: typeof AuthRoleIndexRoute
|
||||||
AuthRoomRoomNameIndexRoute: typeof AuthRoomRoomNameIndexRoute
|
AuthRoomsIndexRoute: typeof AuthRoomsIndexRoute
|
||||||
|
AuthRoleCreateIndexRoute: typeof AuthRoleCreateIndexRoute
|
||||||
|
AuthRoomsRoomNameIndexRoute: typeof AuthRoomsRoomNameIndexRoute
|
||||||
|
AuthRoleIdEditIndexRoute: typeof AuthRoleIdEditIndexRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthRouteChildren: AuthRouteChildren = {
|
const AuthRouteChildren: AuthRouteChildren = {
|
||||||
AuthAgentIndexRoute: AuthAgentIndexRoute,
|
AuthAgentIndexRoute: AuthAgentIndexRoute,
|
||||||
AuthAppsIndexRoute: AuthAppsIndexRoute,
|
AuthAppsIndexRoute: AuthAppsIndexRoute,
|
||||||
AuthBlacklistIndexRoute: AuthBlacklistIndexRoute,
|
AuthBlacklistsIndexRoute: AuthBlacklistsIndexRoute,
|
||||||
AuthCommandIndexRoute: AuthCommandIndexRoute,
|
AuthCommandsIndexRoute: AuthCommandsIndexRoute,
|
||||||
AuthDashboardIndexRoute: AuthDashboardIndexRoute,
|
AuthDashboardIndexRoute: AuthDashboardIndexRoute,
|
||||||
AuthDeviceIndexRoute: AuthDeviceIndexRoute,
|
AuthDeviceIndexRoute: AuthDeviceIndexRoute,
|
||||||
AuthRoomIndexRoute: AuthRoomIndexRoute,
|
AuthRoleIndexRoute: AuthRoleIndexRoute,
|
||||||
AuthRoomRoomNameIndexRoute: AuthRoomRoomNameIndexRoute,
|
AuthRoomsIndexRoute: AuthRoomsIndexRoute,
|
||||||
|
AuthRoleCreateIndexRoute: AuthRoleCreateIndexRoute,
|
||||||
|
AuthRoomsRoomNameIndexRoute: AuthRoomsRoomNameIndexRoute,
|
||||||
|
AuthRoleIdEditIndexRoute: AuthRoleIdEditIndexRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
|
const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,73 @@
|
||||||
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
|
import { useEffect } from "react";
|
||||||
import AppLayout from '@/layouts/app-layout'
|
import AppBreadCrumb from "@/components/app-breadcrumb";
|
||||||
|
import { AppSidebar } from "@/components/sidebars/app-sidebar";
|
||||||
|
import AvatarDropdown from "@/components/avatar-dropdown";
|
||||||
|
import SessionTimeOutErrorPage from "@/components/pages/session-timeout-error";
|
||||||
|
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { createFileRoute, Outlet, redirect, useRouter } from "@tanstack/react-router";
|
||||||
|
import { useUIStore } from "@/stores/uiStore";
|
||||||
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
|
|
||||||
export const Route = createFileRoute('/_auth')({
|
export const Route = createFileRoute("/_auth")({
|
||||||
|
beforeLoad: ({ context, location }) => {
|
||||||
// beforeLoad: async ({context}) => {
|
if (!context.auth.isAuthenticated) {
|
||||||
// const {token} = context.auth
|
throw redirect({
|
||||||
// if (!token) {
|
to: "/login",
|
||||||
// throw redirect({to: '/login'})
|
search: {
|
||||||
// }
|
redirect: location.href
|
||||||
// },
|
}
|
||||||
component: AuthenticatedLayout,
|
});
|
||||||
})
|
}
|
||||||
|
},
|
||||||
function AuthenticatedLayout() {
|
component: RouteComponent
|
||||||
return (
|
});
|
||||||
<AppLayout>
|
|
||||||
<Outlet />
|
function RouteComponent() {
|
||||||
</AppLayout>
|
const auth = useAuth();
|
||||||
)
|
const setCurrent = useUIStore((state) => state.setCurrent);
|
||||||
|
const router = useRouter();
|
||||||
|
const navigate = Route.useNavigate();
|
||||||
|
const currentPath = router.state.location.pathname;
|
||||||
|
|
||||||
|
// Update current path in UI store when location changes
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrent(currentPath);
|
||||||
|
}, [currentPath, setCurrent]);
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
if (window.confirm("Bạn chắc chắn muốn đăng xuất khỏi hệ thống?")) {
|
||||||
|
auth.logout().then(() => {
|
||||||
|
router.invalidate().finally(() => {
|
||||||
|
navigate({ to: "/" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const username = auth.username;
|
||||||
|
|
||||||
|
if (!auth.isAuthenticated) {
|
||||||
|
return <SessionTimeOutErrorPage />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarProvider className="h-screen w-screen">
|
||||||
|
<AppSidebar />
|
||||||
|
<SidebarInset>
|
||||||
|
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
|
||||||
|
<SidebarTrigger className="-ml-1" />
|
||||||
|
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||||
|
<AppBreadCrumb />
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 ml-auto">
|
||||||
|
<AvatarDropdown username={username} role={auth.role} onLogOut={handleLogout} />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div className="flex flex-1 flex-col gap-4 p-4 overflow-x-auto">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</SidebarInset>
|
||||||
|
</SidebarProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -10,10 +10,17 @@ import { toast } from "sonner";
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import type { AxiosProgressEvent } from "axios";
|
import type { AxiosProgressEvent } from "axios";
|
||||||
import type { Version } from "@/types/file";
|
import type { Version } from "@/types/file";
|
||||||
|
import { ErrorFetchingPage } from "@/components/pages/error-fetching-page";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_auth/agent/")({
|
export const Route = createFileRoute("/_auth/agent/")({
|
||||||
head: () => ({ meta: [{ title: "Quản lý Agent" }] }),
|
head: () => ({ meta: [{ title: "Quản lý Agent" }] }),
|
||||||
component: AgentsPage,
|
component: AgentsPage,
|
||||||
|
errorComponent: ErrorFetchingPage,
|
||||||
|
loader: async ({ context }) => {
|
||||||
|
context.breadcrumbs = [
|
||||||
|
{ title: "Quản lý Agent", path: "/_auth/agent/" },
|
||||||
|
];
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function AgentsPage() {
|
function AgentsPage() {
|
||||||
|
|
@ -35,7 +42,7 @@ function AgentsPage() {
|
||||||
|
|
||||||
const handleUpload = async (
|
const handleUpload = async (
|
||||||
fd: FormData,
|
fd: FormData,
|
||||||
config?: { onUploadProgress?: (e: AxiosProgressEvent) => void }
|
config?: { onUploadProgress?: (e: AxiosProgressEvent) => void },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
await uploadMutation.mutateAsync({
|
await uploadMutation.mutateAsync({
|
||||||
|
|
@ -54,7 +61,7 @@ function AgentsPage() {
|
||||||
for (const roomName of roomNames) {
|
for (const roomName of roomNames) {
|
||||||
await updateMutation.mutateAsync({
|
await updateMutation.mutateAsync({
|
||||||
roomName,
|
roomName,
|
||||||
data: {}
|
data: {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
toast.success("Đã gửi yêu cầu update cho các phòng đã chọn!");
|
toast.success("Đã gửi yêu cầu update cho các phòng đã chọn!");
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,11 @@ import { useState } from "react";
|
||||||
export const Route = createFileRoute("/_auth/apps/")({
|
export const Route = createFileRoute("/_auth/apps/")({
|
||||||
head: () => ({ meta: [{ title: "Quản lý phần mềm" }] }),
|
head: () => ({ meta: [{ title: "Quản lý phần mềm" }] }),
|
||||||
component: AppsComponent,
|
component: AppsComponent,
|
||||||
|
loader: async ({ context }) => {
|
||||||
|
context.breadcrumbs = [
|
||||||
|
{ title: "Quản lý phần mềm", path: "/_auth/apps/" },
|
||||||
|
];
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function AppsComponent() {
|
function AppsComponent() {
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,14 @@ import { BlackListManagerTemplate } from "@/template/table-manager-template";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_auth/blacklist/")({
|
export const Route = createFileRoute("/_auth/blacklists/")({
|
||||||
head: () => ({ meta: [{ title: "Danh sách các ứng dụng bị chặn" }] }),
|
head: () => ({ meta: [{ title: "Danh sách các ứng dụng bị chặn" }] }),
|
||||||
component: BlacklistComponent,
|
component: BlacklistComponent,
|
||||||
|
loader: async ({ context }) => {
|
||||||
|
context.breadcrumbs = [
|
||||||
|
{ title: "Quản lý danh sách chặn", path: "/_auth/blacklists/" },
|
||||||
|
];
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function BlacklistComponent() {
|
function BlacklistComponent() {
|
||||||
|
|
@ -13,16 +13,19 @@ import {
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Check, X, Edit2, Trash2 } from "lucide-react";
|
import { Check, X, Edit2, Trash2 } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import type { ShellCommandData } from "@/components/forms/command-form";
|
import type { ShellCommandData } from "@/components/forms/command-form";
|
||||||
import type { CommandRegistry } from "@/types/command-registry";
|
import type { CommandRegistry } from "@/types/command-registry";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_auth/command/")({
|
export const Route = createFileRoute("/_auth/commands/")({
|
||||||
head: () => ({ meta: [{ title: "Gửi lệnh từ xa" }] }),
|
head: () => ({ meta: [{ title: "Gửi lệnh từ xa" }] }),
|
||||||
component: CommandPage,
|
component: CommandPage,
|
||||||
|
loader: async ({ context }) => {
|
||||||
|
context.breadcrumbs = [
|
||||||
|
{ title: "Quản lý lệnh", path: "/_auth/commands/" },
|
||||||
|
];
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function CommandPage() {
|
function CommandPage() {
|
||||||
|
|
@ -2,6 +2,12 @@ import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
export const Route = createFileRoute('/_auth/dashboard/')({
|
export const Route = createFileRoute('/_auth/dashboard/')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
|
head: () => ({ meta: [{ title: 'Dashboard' }] }),
|
||||||
|
loader: async ({ context }) => {
|
||||||
|
context.breadcrumbs = [
|
||||||
|
{ title: "Dashboard", path: "/_auth/dashboard/" },
|
||||||
|
];
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
|
|
|
||||||
311
src/routes/_auth/role/$id/edit/index.tsx
Normal file
311
src/routes/_auth/role/$id/edit/index.tsx
Normal file
|
|
@ -0,0 +1,311 @@
|
||||||
|
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||||
|
import {
|
||||||
|
useGetRoleById,
|
||||||
|
useGetRolePermissions,
|
||||||
|
useGetPermissionList,
|
||||||
|
useToggleRolePermission,
|
||||||
|
useAssignRolePermissions,
|
||||||
|
} from "@/hooks/queries";
|
||||||
|
import { useState, useEffect, useMemo } from "react";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Shield, ArrowLeft, Save, RefreshCw } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import type { Permission, PermissionOnRole } from "@/types/permission";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_auth/role/$id/edit/")({
|
||||||
|
component: EditRolePermissionsComponent,
|
||||||
|
loader: async ({ context, params }) => {
|
||||||
|
context.breadcrumbs = [
|
||||||
|
{ title: "Quản lý role", path: "/role" },
|
||||||
|
{ title: "Chỉnh sửa quyền", path: `/role/${params.id}/edit` },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function EditRolePermissionsComponent() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { id } = Route.useParams();
|
||||||
|
const roleId = Number(id);
|
||||||
|
|
||||||
|
// Queries
|
||||||
|
const { data: role, isLoading: roleLoading } = useGetRoleById(roleId);
|
||||||
|
const { data: rolePermissions = [], isLoading: rolePermissionsLoading } = useGetRolePermissions(roleId);
|
||||||
|
const { data: allPermissions = [], isLoading: permissionsLoading } = useGetPermissionList();
|
||||||
|
|
||||||
|
// Mutations
|
||||||
|
const toggleMutation = useToggleRolePermission();
|
||||||
|
const assignMutation = useAssignRolePermissions();
|
||||||
|
|
||||||
|
// State
|
||||||
|
const [selectedPermissions, setSelectedPermissions] = useState<number[]>([]);
|
||||||
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
|
||||||
|
// Initialize selected permissions from role's current permissions
|
||||||
|
useEffect(() => {
|
||||||
|
if (rolePermissions && Array.isArray(rolePermissions)) {
|
||||||
|
// Use permissionEnum as the identifier (matches value from permission list)
|
||||||
|
const checkedPermissions = rolePermissions
|
||||||
|
.filter((p: PermissionOnRole) => p.isChecked === 1)
|
||||||
|
.map((p: PermissionOnRole) => p.permissionEnum);
|
||||||
|
setSelectedPermissions(checkedPermissions);
|
||||||
|
}
|
||||||
|
}, [rolePermissions]);
|
||||||
|
|
||||||
|
// Group permissions by parent (category)
|
||||||
|
const groupedPermissions = useMemo(() => {
|
||||||
|
const groups: Record<string, Permission[]> = {};
|
||||||
|
const permissionList = Array.isArray(allPermissions) ? allPermissions : [];
|
||||||
|
|
||||||
|
// First pass: identify all parent categories
|
||||||
|
const parentPermissions: Permission[] = [];
|
||||||
|
const childPermissions: Permission[] = [];
|
||||||
|
|
||||||
|
permissionList.forEach((perm: Permission) => {
|
||||||
|
const permValue = perm.value ?? perm.enum ?? 0;
|
||||||
|
const isParent = permValue % 10 === 0;
|
||||||
|
if (isParent) {
|
||||||
|
parentPermissions.push(perm);
|
||||||
|
groups[perm.name] = [];
|
||||||
|
} else {
|
||||||
|
childPermissions.push(perm);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Second pass: assign children to parent categories
|
||||||
|
childPermissions.forEach((perm: Permission) => {
|
||||||
|
const permValue = perm.value ?? perm.enum ?? 0;
|
||||||
|
const parentEnum = Math.floor(permValue / 10) * 10;
|
||||||
|
const parent = parentPermissions.find((p: Permission) => (p.value ?? p.enum) === parentEnum);
|
||||||
|
const parentName = parent?.name || "Khác";
|
||||||
|
if (!groups[parentName]) {
|
||||||
|
groups[parentName] = [];
|
||||||
|
}
|
||||||
|
groups[parentName].push(perm);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Third pass: add parent permissions that have no children as selectable items
|
||||||
|
parentPermissions.forEach((parent) => {
|
||||||
|
const parentValue = parent.value ?? parent.enum ?? 0;
|
||||||
|
const hasChildren = childPermissions.some((child) => {
|
||||||
|
const childValue = child.value ?? child.enum ?? 0;
|
||||||
|
return Math.floor(childValue / 10) * 10 === parentValue;
|
||||||
|
});
|
||||||
|
if (!hasChildren) {
|
||||||
|
groups[parent.name].push(parent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove empty groups
|
||||||
|
Object.keys(groups).forEach((key) => {
|
||||||
|
if (groups[key].length === 0) {
|
||||||
|
delete groups[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}, [allPermissions]);
|
||||||
|
|
||||||
|
// Helper to get unique identifier for permission (use value as ID)
|
||||||
|
const getPermId = (perm: Permission) => perm.value ?? perm.id ?? 0;
|
||||||
|
|
||||||
|
const handleTogglePermission = (permissionValue: number) => {
|
||||||
|
setSelectedPermissions((prev) =>
|
||||||
|
prev.includes(permissionValue)
|
||||||
|
? prev.filter((v) => v !== permissionValue)
|
||||||
|
: [...prev, permissionValue]
|
||||||
|
);
|
||||||
|
setHasChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectAll = (categoryPermissions: Permission[]) => {
|
||||||
|
const allValues = categoryPermissions.map((p) => getPermId(p));
|
||||||
|
const allSelected = allValues.every((v) => selectedPermissions.includes(v));
|
||||||
|
|
||||||
|
if (allSelected) {
|
||||||
|
setSelectedPermissions((prev) =>
|
||||||
|
prev.filter((v) => !allValues.includes(v))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedPermissions((prev) => [...new Set([...prev, ...allValues])]);
|
||||||
|
}
|
||||||
|
setHasChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save all permissions at once
|
||||||
|
const handleSaveAll = async () => {
|
||||||
|
try {
|
||||||
|
await assignMutation.mutateAsync({
|
||||||
|
roleId,
|
||||||
|
permissionIds: selectedPermissions,
|
||||||
|
});
|
||||||
|
toast.success("Cập nhật quyền thành công!");
|
||||||
|
setHasChanges(false);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Cập nhật quyền thất bại!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLoading = roleLoading || rolePermissionsLoading || permissionsLoading;
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="w-full px-6 flex items-center justify-center h-64">
|
||||||
|
<RefreshCw className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 flex items-center gap-2">
|
||||||
|
Chỉnh sửa quyền:
|
||||||
|
<Badge variant="secondary" className="text-lg">
|
||||||
|
{role?.roleName}
|
||||||
|
</Badge>
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground mt-2">
|
||||||
|
Quản lý quyền hạn của role này
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" onClick={() => navigate({ to: "/role" })}>
|
||||||
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||||
|
Quay lại
|
||||||
|
</Button>
|
||||||
|
{hasChanges && (
|
||||||
|
<Button onClick={handleSaveAll} disabled={assignMutation.isPending}>
|
||||||
|
<Save className="h-4 w-4 mr-2" />
|
||||||
|
{assignMutation.isPending ? "Đang lưu..." : "Lưu thay đổi"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Role Info */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Shield className="h-5 w-5" /> Thông tin Role
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-muted-foreground">Tên Role</span>
|
||||||
|
<p className="font-medium">{role?.roleName}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-muted-foreground">Độ ưu tiên</span>
|
||||||
|
<p className="font-medium">{role?.priority}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-muted-foreground">Số quyền đã gán</span>
|
||||||
|
<p className="font-medium">{selectedPermissions.length}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Permissions */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Quyền hạn</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Tick chọn để bật/tắt quyền ({selectedPermissions.length} đang được gán)
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ScrollArea className="h-[500px] pr-4">
|
||||||
|
<div className="space-y-6">
|
||||||
|
{Object.entries(groupedPermissions).map(([category, perms]) => (
|
||||||
|
<div key={category} className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between border-b pb-2">
|
||||||
|
<h4 className="font-semibold text-sm text-muted-foreground uppercase">
|
||||||
|
{category}
|
||||||
|
</h4>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleSelectAll(perms)}
|
||||||
|
>
|
||||||
|
{perms.every((p) => selectedPermissions.includes(getPermId(p)))
|
||||||
|
? "Bỏ tất cả"
|
||||||
|
: "Chọn tất cả"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||||
|
{perms.map((perm) => {
|
||||||
|
const permValue = getPermId(perm);
|
||||||
|
const isChecked = selectedPermissions.includes(permValue);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={permValue}
|
||||||
|
className={`flex items-center space-x-2 p-2 rounded border hover:bg-muted/50 transition-colors ${
|
||||||
|
isChecked ? "bg-primary/5 border-primary/30" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={`perm-${permValue}`}
|
||||||
|
checked={isChecked}
|
||||||
|
onCheckedChange={() => handleTogglePermission(permValue)}
|
||||||
|
disabled={toggleMutation.isPending}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor={`perm-${permValue}`}
|
||||||
|
className="text-sm cursor-pointer flex-1"
|
||||||
|
>
|
||||||
|
{perm.name}
|
||||||
|
<span className="text-xs text-muted-foreground ml-1">
|
||||||
|
({permValue})
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Footer Actions */}
|
||||||
|
{hasChanges && (
|
||||||
|
<div className="flex justify-end gap-2 sticky bottom-4 bg-background p-4 rounded-lg border shadow-lg">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
// Reset to original - use permissionEnum as identifier
|
||||||
|
const checkedPermissions = (rolePermissions as PermissionOnRole[])
|
||||||
|
.filter((p) => p.isChecked === 1)
|
||||||
|
.map((p) => p.permissionEnum);
|
||||||
|
setSelectedPermissions(checkedPermissions);
|
||||||
|
setHasChanges(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Hủy thay đổi
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSaveAll} disabled={assignMutation.isPending}>
|
||||||
|
<Save className="h-4 w-4 mr-2" />
|
||||||
|
{assignMutation.isPending ? "Đang lưu..." : "Lưu tất cả thay đổi"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
273
src/routes/_auth/role/create/index.tsx
Normal file
273
src/routes/_auth/role/create/index.tsx
Normal file
|
|
@ -0,0 +1,273 @@
|
||||||
|
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||||
|
import { useGetPermissionList, useCreateRole } from "@/hooks/queries";
|
||||||
|
import { useState, useMemo } from "react";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { Shield, ArrowLeft, Save } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import type { Permission } from "@/types/permission";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_auth/role/create/")({
|
||||||
|
component: CreateRoleComponent,
|
||||||
|
loader: async ({ context }) => {
|
||||||
|
context.breadcrumbs = [
|
||||||
|
{ title: "Quản lý role", path: "/role" },
|
||||||
|
{ title: "Tạo role mới", path: "/role/create" },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function CreateRoleComponent() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { data: permissions = [], isLoading: permissionsLoading } = useGetPermissionList();
|
||||||
|
const createMutation = useCreateRole();
|
||||||
|
|
||||||
|
const [roleName, setRoleName] = useState("");
|
||||||
|
const [priority, setPriority] = useState(0);
|
||||||
|
const [selectedPermissions, setSelectedPermissions] = useState<number[]>([]);
|
||||||
|
|
||||||
|
// Helper to get unique identifier for permission
|
||||||
|
const getPermValue = (perm: Permission) => perm.value ?? perm.id ?? 0;
|
||||||
|
|
||||||
|
// Group permissions by parent (category)
|
||||||
|
const groupedPermissions = useMemo(() => {
|
||||||
|
const groups: Record<string, Permission[]> = {};
|
||||||
|
const permissionList = Array.isArray(permissions) ? permissions : [];
|
||||||
|
|
||||||
|
// First pass: identify all parent categories
|
||||||
|
const parentPermissions: Permission[] = [];
|
||||||
|
const childPermissions: Permission[] = [];
|
||||||
|
|
||||||
|
permissionList.forEach((perm: Permission) => {
|
||||||
|
const permValue = perm.value ?? perm.enum ?? 0;
|
||||||
|
const isParent = permValue % 10 === 0;
|
||||||
|
if (isParent) {
|
||||||
|
parentPermissions.push(perm);
|
||||||
|
groups[perm.name] = [];
|
||||||
|
} else {
|
||||||
|
childPermissions.push(perm);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Second pass: assign children to parent categories
|
||||||
|
childPermissions.forEach((perm: Permission) => {
|
||||||
|
const permValue = perm.value ?? perm.enum ?? 0;
|
||||||
|
const parentEnum = Math.floor(permValue / 10) * 10;
|
||||||
|
const parent = parentPermissions.find((p: Permission) => (p.value ?? p.enum) === parentEnum);
|
||||||
|
const parentName = parent?.name || "Khác";
|
||||||
|
if (!groups[parentName]) {
|
||||||
|
groups[parentName] = [];
|
||||||
|
}
|
||||||
|
groups[parentName].push(perm);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Third pass: add parent permissions that have no children as selectable items
|
||||||
|
// (like ALLOW_ALL which is value 0 with no children)
|
||||||
|
parentPermissions.forEach((parent) => {
|
||||||
|
const parentValue = parent.value ?? parent.enum ?? 0;
|
||||||
|
// Check if this parent has any children
|
||||||
|
const hasChildren = childPermissions.some((child) => {
|
||||||
|
const childValue = child.value ?? child.enum ?? 0;
|
||||||
|
return Math.floor(childValue / 10) * 10 === parentValue;
|
||||||
|
});
|
||||||
|
// If no children, add the parent itself as a selectable item
|
||||||
|
if (!hasChildren) {
|
||||||
|
groups[parent.name].push(parent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove empty groups
|
||||||
|
Object.keys(groups).forEach((key) => {
|
||||||
|
if (groups[key].length === 0) {
|
||||||
|
delete groups[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}, [permissions]);
|
||||||
|
|
||||||
|
const handleTogglePermission = (permissionValue: number) => {
|
||||||
|
setSelectedPermissions((prev) =>
|
||||||
|
prev.includes(permissionValue)
|
||||||
|
? prev.filter((v) => v !== permissionValue)
|
||||||
|
: [...prev, permissionValue]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectAll = (categoryPermissions: Permission[]) => {
|
||||||
|
const allValues = categoryPermissions.map((p) => getPermValue(p));
|
||||||
|
const allSelected = allValues.every((v) => selectedPermissions.includes(v));
|
||||||
|
|
||||||
|
if (allSelected) {
|
||||||
|
setSelectedPermissions((prev) =>
|
||||||
|
prev.filter((v) => !allValues.includes(v))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedPermissions((prev) => [...new Set([...prev, ...allValues])]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!roleName.trim()) {
|
||||||
|
toast.error("Vui lòng nhập tên role!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createMutation.mutateAsync({
|
||||||
|
RoleName: roleName,
|
||||||
|
Priority: priority,
|
||||||
|
PermissionIds: selectedPermissions,
|
||||||
|
});
|
||||||
|
toast.success("Tạo role thành công!");
|
||||||
|
navigate({ to: "/role" });
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Tạo role thất bại!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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">Tạo Role mới</h1>
|
||||||
|
<p className="text-muted-foreground mt-2">
|
||||||
|
Tạo vai trò mới và gán quyền hạn
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" onClick={() => navigate({ to: "/role" })}>
|
||||||
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||||
|
Quay lại
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
{/* Role Info */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Shield className="h-5 w-5" /> Thông tin Role
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Nhập thông tin cơ bản của role
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="roleName">Tên Role *</Label>
|
||||||
|
<Input
|
||||||
|
id="roleName"
|
||||||
|
value={roleName}
|
||||||
|
onChange={(e) => setRoleName(e.target.value)}
|
||||||
|
placeholder="Nhập tên role..."
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="priority">Độ ưu tiên</Label>
|
||||||
|
<Input
|
||||||
|
id="priority"
|
||||||
|
type="number"
|
||||||
|
value={priority}
|
||||||
|
onChange={(e) => setPriority(Number(e.target.value))}
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Permissions Selection */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Chọn quyền hạn</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Chọn các quyền mà role này được phép thực hiện ({selectedPermissions.length} đã chọn)
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{permissionsLoading ? (
|
||||||
|
<div className="text-center py-4">Đang tải danh sách quyền...</div>
|
||||||
|
) : (
|
||||||
|
<ScrollArea className="h-[400px] pr-4">
|
||||||
|
<div className="space-y-6">
|
||||||
|
{Object.entries(groupedPermissions).map(([category, perms]) => (
|
||||||
|
<div key={category} className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h4 className="font-semibold text-sm text-muted-foreground uppercase">
|
||||||
|
{category}
|
||||||
|
</h4>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleSelectAll(perms)}
|
||||||
|
>
|
||||||
|
{perms.every((p) => selectedPermissions.includes(getPermValue(p)))
|
||||||
|
? "Bỏ tất cả"
|
||||||
|
: "Chọn tất cả"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||||
|
{perms.map((perm) => {
|
||||||
|
const permValue = getPermValue(perm);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={permValue}
|
||||||
|
className="flex items-center space-x-2 p-2 rounded border hover:bg-muted/50"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={`perm-${permValue}`}
|
||||||
|
checked={selectedPermissions.includes(permValue)}
|
||||||
|
onCheckedChange={() => handleTogglePermission(permValue)}
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor={`perm-${permValue}`}
|
||||||
|
className="text-sm cursor-pointer flex-1"
|
||||||
|
>
|
||||||
|
{perm.name}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Submit */}
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => navigate({ to: "/role" })}
|
||||||
|
>
|
||||||
|
Hủy
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={createMutation.isPending}>
|
||||||
|
<Save className="h-4 w-4 mr-2" />
|
||||||
|
{createMutation.isPending ? "Đang tạo..." : "Tạo Role"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
132
src/routes/_auth/role/index.tsx
Normal file
132
src/routes/_auth/role/index.tsx
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
import { useGetRoleList, useDeleteRole } from "@/hooks/queries";
|
||||||
|
import { formatDate } from "@/lib/utils";
|
||||||
|
import type { TRoleResponse } from "@/types/role";
|
||||||
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||||
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import { RoleManagerTemplate } from "@/template/role-manager-template";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Pencil, Trash2, Shield } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_auth/role/")({
|
||||||
|
component: RoleComponent,
|
||||||
|
loader: async ({ context }) => {
|
||||||
|
context.breadcrumbs = [
|
||||||
|
{
|
||||||
|
title: "Quản lý role",
|
||||||
|
path: "#",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Danh sách role",
|
||||||
|
path: "/role",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function RoleComponent() {
|
||||||
|
const { data: roles = [], isLoading } = useGetRoleList();
|
||||||
|
const roleList = Array.isArray(roles) ? roles : [roles];
|
||||||
|
const deleteMutation = useDeleteRole();
|
||||||
|
|
||||||
|
const handleDelete = async (id: number, roleName: string) => {
|
||||||
|
if (window.confirm(`Bạn có chắc chắn muốn xóa role "${roleName}"?`)) {
|
||||||
|
try {
|
||||||
|
await deleteMutation.mutateAsync(id);
|
||||||
|
toast.success("Xóa role thành công!");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Xóa role thất bại!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: ColumnDef<TRoleResponse>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: "roleName",
|
||||||
|
header: () => <div className="font-bold text-center">Role</div>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="text-center font-medium">{row.original.roleName}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "priority",
|
||||||
|
header: () => <div className="font-bold text-center">Độ ưu tiên</div>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="text-center">{row.original.priority}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "createdAt",
|
||||||
|
header: () => <div className="font-bold text-center">Ngày tạo</div>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="text-center">
|
||||||
|
{formatDate(row.original.createdAt)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "createdBy",
|
||||||
|
header: () => <div className="font-bold text-center">Người tạo</div>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="text-center">{row.original.createdBy || "-"}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "updatedAt",
|
||||||
|
header: () => <div className="font-bold text-center">Ngày cập nhật</div>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="text-center">
|
||||||
|
{formatDate(row.original.updatedAt)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "updatedBy",
|
||||||
|
header: () => <div className="font-bold text-center">Người cập nhật</div>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="text-center">{row.original.updatedBy || "-"}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
header: () => <div className="font-bold text-center">Hành động</div>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<Link
|
||||||
|
to="/role/$id/edit"
|
||||||
|
params={{ id: String(row.original.id) }}
|
||||||
|
>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<Pencil className="h-4 w-4 mr-1" />
|
||||||
|
Sửa quyền
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDelete(row.original.id, row.original.roleName)}
|
||||||
|
disabled={deleteMutation.isPending}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-1" />
|
||||||
|
Xóa
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RoleManagerTemplate<TRoleResponse>
|
||||||
|
title="Quản lý Role"
|
||||||
|
description="Quản lý các vai trò và quyền hạn trong hệ thống"
|
||||||
|
data={roleList}
|
||||||
|
isLoading={isLoading}
|
||||||
|
columns={columns}
|
||||||
|
icon={Shield}
|
||||||
|
tableTitle="Danh sách Role"
|
||||||
|
tableDescription="Các vai trò trong hệ thống và quyền hạn tương ứng"
|
||||||
|
createButtonLabel="Tạo role mới"
|
||||||
|
createLink="/role/create"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -12,7 +12,7 @@ import { useMachineNumber } from "@/hooks/useMachineNumber";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { CommandActionButtons } from "@/components/buttons/command-action-buttons";
|
import { CommandActionButtons } from "@/components/buttons/command-action-buttons";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_auth/room/$roomName/")({
|
export const Route = createFileRoute("/_auth/rooms/$roomName/")({
|
||||||
head: ({ params }) => ({
|
head: ({ params }) => ({
|
||||||
meta: [{ title: `Danh sách thiết bị phòng ${params.roomName}` }],
|
meta: [{ title: `Danh sách thiết bị phòng ${params.roomName}` }],
|
||||||
}),
|
}),
|
||||||
|
|
@ -20,7 +20,7 @@ export const Route = createFileRoute("/_auth/room/$roomName/")({
|
||||||
});
|
});
|
||||||
|
|
||||||
function RoomDetailPage() {
|
function RoomDetailPage() {
|
||||||
const { roomName } = useParams({ from: "/_auth/room/$roomName/" });
|
const { roomName } = useParams({ from: "/_auth/rooms/$roomName/" });
|
||||||
const [viewMode, setViewMode] = useState<"grid" | "table">("grid");
|
const [viewMode, setViewMode] = useState<"grid" | "table">("grid");
|
||||||
const [isCheckingFolder, setIsCheckingFolder] = useState(false);
|
const [isCheckingFolder, setIsCheckingFolder] = useState(false);
|
||||||
|
|
||||||
|
|
@ -31,7 +31,7 @@ import React from "react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_auth/room/")({
|
export const Route = createFileRoute("/_auth/rooms/")({
|
||||||
head: () => ({
|
head: () => ({
|
||||||
meta: [{ title: "Danh sách phòng" }],
|
meta: [{ title: "Danh sách phòng" }],
|
||||||
}),
|
}),
|
||||||
|
|
@ -1,25 +1,39 @@
|
||||||
import {
|
import { Button } from "@/components/ui/button";
|
||||||
createFileRoute,
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
Outlet,
|
import { createFileRoute, Link, redirect } from "@tanstack/react-router";
|
||||||
} from "@tanstack/react-router";
|
|
||||||
import AppLayout from "@/layouts/app-layout";
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
head: () => ({
|
beforeLoad: ({ context }) => {
|
||||||
meta: [
|
if (!context.auth.isAuthenticated) {
|
||||||
{
|
throw redirect({
|
||||||
title: "Dashboard",
|
to: "/login",
|
||||||
|
search: {
|
||||||
|
redirect: location.pathname
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw redirect({ to: "/dashboard" });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
],
|
component: Index
|
||||||
}),
|
|
||||||
component: App,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function App() {
|
function Index() {
|
||||||
|
const auth = useAuth();
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col min-h-[70vh] items-center justify-center text-2xl">
|
||||||
<AppLayout>
|
<h1>Access Control</h1>
|
||||||
<Outlet />
|
<div className="mt-4">
|
||||||
</AppLayout>
|
{auth.isAuthenticated ? (
|
||||||
</>
|
<Link to="/dashboard">
|
||||||
|
<Button>Trang chủ</Button>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<Link to="/login">
|
||||||
|
<Button>Đăng nhập</Button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,8 @@ export * as deviceCommService from "./device-comm.service";
|
||||||
// Command API Services
|
// Command API Services
|
||||||
export * as commandService from "./command.service";
|
export * as commandService from "./command.service";
|
||||||
|
|
||||||
|
// Permission API Services
|
||||||
|
export * as permissionService from "./permission.service";
|
||||||
|
|
||||||
|
// Role API Services
|
||||||
|
export * as roleService from "./role.service";
|
||||||
|
|
|
||||||
71
src/services/permission.service.ts
Normal file
71
src/services/permission.service.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import axios from "axios";
|
||||||
|
import { API_ENDPOINTS } from "@/config/api";
|
||||||
|
import type { Permission } from "@/types/permission";
|
||||||
|
|
||||||
|
// Helper to extract data from wrapped or unwrapped response
|
||||||
|
// Handles both { success, data: T } and { success, data: { data: T, total } }
|
||||||
|
function extractData<T>(responseData: any): T {
|
||||||
|
if (responseData && typeof responseData === 'object' && 'success' in responseData && 'data' in responseData) {
|
||||||
|
const innerData = responseData.data;
|
||||||
|
// Check for double-wrapped paginated response: { data: [...], total: n }
|
||||||
|
if (innerData && typeof innerData === 'object' && 'data' in innerData && 'total' in innerData) {
|
||||||
|
return innerData.data as T;
|
||||||
|
}
|
||||||
|
return innerData as T;
|
||||||
|
}
|
||||||
|
return responseData as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lấy danh sách permission từ enum
|
||||||
|
*/
|
||||||
|
export async function getPermissionList(): Promise<Permission[]> {
|
||||||
|
const response = await axios.get(API_ENDPOINTS.PERMISSION.GET_LIST);
|
||||||
|
return extractData<Permission[]>(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lấy permission theo category
|
||||||
|
*/
|
||||||
|
export async function getPermissionByCategory(): Promise<Record<string, Permission[]>> {
|
||||||
|
const response = await axios.get(
|
||||||
|
API_ENDPOINTS.PERMISSION.GET_BY_CATEGORY
|
||||||
|
);
|
||||||
|
return extractData<Record<string, Permission[]>>(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lấy chi tiết permission theo value
|
||||||
|
* @param value - Giá trị enum của permission
|
||||||
|
*/
|
||||||
|
export async function getPermissionByValue(value: number): Promise<Permission> {
|
||||||
|
const response = await axios.get(
|
||||||
|
API_ENDPOINTS.PERMISSION.GET_BY_VALUE(value)
|
||||||
|
);
|
||||||
|
return extractData<Permission>(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import permission từ enum vào DB (chạy 1 lần đầu tiên)
|
||||||
|
*/
|
||||||
|
export async function seedPermissionFromEnum(): Promise<any> {
|
||||||
|
const response = await axios.post(API_ENDPOINTS.PERMISSION.SEED_FROM_ENUM);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lấy danh sách permission từ database
|
||||||
|
*/
|
||||||
|
export async function getPermissionDbList(): Promise<Permission[]> {
|
||||||
|
const response = await axios.get(API_ENDPOINTS.PERMISSION.GET_DB_LIST);
|
||||||
|
return extractData<Permission[]>(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Xóa permission
|
||||||
|
* @param id - ID của permission
|
||||||
|
*/
|
||||||
|
export async function deletePermission(id: number): Promise<any> {
|
||||||
|
const response = await axios.delete(API_ENDPOINTS.PERMISSION.DELETE(id));
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
118
src/services/role.service.ts
Normal file
118
src/services/role.service.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
import axios from "axios";
|
||||||
|
import { API_ENDPOINTS } from "@/config/api";
|
||||||
|
import type { TCreateRoleRequestBody, TRoleResponse } from "@/types/role";
|
||||||
|
import type { PermissionOnRole } from "@/types/permission";
|
||||||
|
|
||||||
|
|
||||||
|
// Helper to extract data from wrapped or unwrapped response
|
||||||
|
// Handles both { success, data: T } and { success, data: { data: T, total } }
|
||||||
|
function extractData<T>(responseData: any): T {
|
||||||
|
if (responseData && typeof responseData === 'object' && 'success' in responseData && 'data' in responseData) {
|
||||||
|
const innerData = responseData.data;
|
||||||
|
// Check for double-wrapped paginated response: { data: [...], total: n }
|
||||||
|
if (innerData && typeof innerData === 'object' && 'data' in innerData && 'total' in innerData) {
|
||||||
|
return innerData.data as T;
|
||||||
|
}
|
||||||
|
return innerData as T;
|
||||||
|
}
|
||||||
|
return responseData as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lấy danh sách tất cả roles
|
||||||
|
*/
|
||||||
|
export async function getRoleList(): Promise<TRoleResponse[]> {
|
||||||
|
const response = await axios.get(API_ENDPOINTS.ROLE.GET_LIST);
|
||||||
|
return extractData<TRoleResponse[]>(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lấy chi tiết role theo ID
|
||||||
|
* @param id - ID của role
|
||||||
|
*/
|
||||||
|
export async function getRoleById(id: number): Promise<TRoleResponse> {
|
||||||
|
const response = await axios.get(API_ENDPOINTS.ROLE.GET_BY_ID(id));
|
||||||
|
return extractData<TRoleResponse>(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tạo role mới
|
||||||
|
* @param data - Dữ liệu role mới
|
||||||
|
*/
|
||||||
|
export async function createRole(data: TCreateRoleRequestBody): Promise<TRoleResponse> {
|
||||||
|
const response = await axios.post<TRoleResponse>(API_ENDPOINTS.ROLE.CREATE, data);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cập nhật role
|
||||||
|
* @param id - ID của role
|
||||||
|
* @param data - Dữ liệu cập nhật
|
||||||
|
*/
|
||||||
|
export async function updateRole(
|
||||||
|
id: number,
|
||||||
|
data: Partial<TCreateRoleRequestBody>
|
||||||
|
): Promise<TRoleResponse> {
|
||||||
|
const response = await axios.put<TRoleResponse>(
|
||||||
|
API_ENDPOINTS.ROLE.UPDATE(id),
|
||||||
|
data
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Xóa role
|
||||||
|
* @param id - ID của role
|
||||||
|
*/
|
||||||
|
export async function deleteRole(id: number): Promise<any> {
|
||||||
|
const response = await axios.delete(API_ENDPOINTS.ROLE.DELETE(id));
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lấy danh sách permissions của role
|
||||||
|
* @param id - ID của role
|
||||||
|
*/
|
||||||
|
export async function getRolePermissions(id: number): Promise<PermissionOnRole[]> {
|
||||||
|
const response = await axios.get(
|
||||||
|
API_ENDPOINTS.ROLE.GET_PERMISSIONS(id)
|
||||||
|
);
|
||||||
|
// API returns { success, data: { roleId, roleName, permissions: [...] } }
|
||||||
|
const data = extractData<{ roleId: number; roleName: string; permissions: PermissionOnRole[] }>(response.data);
|
||||||
|
return data.permissions || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gán permissions cho role (thay thế toàn bộ)
|
||||||
|
* @param id - ID của role
|
||||||
|
* @param permissionIds - Danh sách ID permissions
|
||||||
|
*/
|
||||||
|
export async function assignRolePermissions(
|
||||||
|
id: number,
|
||||||
|
permissionIds: number[]
|
||||||
|
): Promise<any> {
|
||||||
|
const response = await axios.post(
|
||||||
|
API_ENDPOINTS.ROLE.ASSIGN_PERMISSIONS(id),
|
||||||
|
{ permissionIds }
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bật/tắt một permission cụ thể
|
||||||
|
* @param roleId - ID của role
|
||||||
|
* @param permissionId - ID của permission
|
||||||
|
* @param isChecked - Trạng thái bật/tắt
|
||||||
|
*/
|
||||||
|
export async function toggleRolePermission(
|
||||||
|
roleId: number,
|
||||||
|
permissionId: number,
|
||||||
|
isChecked: boolean
|
||||||
|
): Promise<any> {
|
||||||
|
const response = await axios.patch(
|
||||||
|
API_ENDPOINTS.ROLE.TOGGLE_PERMISSION(roleId, permissionId),
|
||||||
|
null,
|
||||||
|
{ params: { isChecked } }
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
48
src/stores/uiStore.ts
Normal file
48
src/stores/uiStore.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { appSidebarSection } from "@/types/app-sidebar";
|
||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
interface UIState {
|
||||||
|
isSidebarCollapsed: boolean;
|
||||||
|
current: number;
|
||||||
|
setCurrent: (value: number | string) => void;
|
||||||
|
getCurrentPath: (current: number) => string[];
|
||||||
|
toggleSidebar: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = appSidebarSection;
|
||||||
|
|
||||||
|
export const useUIStore = create<UIState>((set, get) => ({
|
||||||
|
isSidebarCollapsed: true,
|
||||||
|
toggleSidebar: () => {
|
||||||
|
set({ isSidebarCollapsed: !get().isSidebarCollapsed });
|
||||||
|
},
|
||||||
|
current: localStorage.getItem("current") ? parseInt(localStorage.getItem("current")!) : 0,
|
||||||
|
setCurrent: (value: number | string) => {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
for (const item of data.navMain) {
|
||||||
|
for (const subItem of item.items) {
|
||||||
|
if (subItem.url === value) {
|
||||||
|
if (subItem.code !== undefined) {
|
||||||
|
value = subItem.code;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof value === "string") {
|
||||||
|
value = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
localStorage.setItem("current", value.toString());
|
||||||
|
set({ current: value });
|
||||||
|
},
|
||||||
|
getCurrentPath: (current: number) => {
|
||||||
|
for (const section of data.navMain) {
|
||||||
|
const item = section.items.find((item) => item.code === current);
|
||||||
|
if (item) {
|
||||||
|
return [section.title, item.title];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}));
|
||||||
87
src/template/role-manager-template.tsx
Normal file
87
src/template/role-manager-template.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { VersionTable } from "@/components/tables/version-table";
|
||||||
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import { Plus, Shield, type LucideIcon } from "lucide-react";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
interface RoleManagerTemplateProps<TData> {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
data: TData[];
|
||||||
|
isLoading: boolean;
|
||||||
|
columns: ColumnDef<TData, any>[];
|
||||||
|
onTableInit?: (table: any) => void;
|
||||||
|
// Optional customization
|
||||||
|
icon?: LucideIcon;
|
||||||
|
tableTitle?: string;
|
||||||
|
tableDescription?: string;
|
||||||
|
createButtonLabel?: string;
|
||||||
|
createLink?: string;
|
||||||
|
headerActions?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RoleManagerTemplate<TData>({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
columns,
|
||||||
|
onTableInit,
|
||||||
|
icon: Icon = Shield,
|
||||||
|
tableTitle = "Danh sách",
|
||||||
|
tableDescription,
|
||||||
|
createButtonLabel = "Tạo mới",
|
||||||
|
createLink,
|
||||||
|
headerActions,
|
||||||
|
}: RoleManagerTemplateProps<TData>) {
|
||||||
|
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>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{headerActions}
|
||||||
|
{createLink && (
|
||||||
|
<Link to={createLink}>
|
||||||
|
<Button>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
{createButtonLabel}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Icon className="h-5 w-5" /> {tableTitle}
|
||||||
|
</CardTitle>
|
||||||
|
{tableDescription && (
|
||||||
|
<CardDescription>{tableDescription}</CardDescription>
|
||||||
|
)}
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<VersionTable
|
||||||
|
data={data}
|
||||||
|
isLoading={isLoading}
|
||||||
|
columns={columns}
|
||||||
|
onTableInit={onTableInit}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,59 +1,92 @@
|
||||||
import { AppWindow, Building, CircleX, Home, Terminal } from "lucide-react";
|
import { AppWindow, Building, CircleX, Home, ShieldCheck, Terminal} from "lucide-react";
|
||||||
import { PermissionEnum } from "./permission";
|
import { PermissionEnum } from "./permission";
|
||||||
|
|
||||||
enum AppSidebarSectionCode {
|
enum AppSidebarSectionCode {
|
||||||
DASHBOARD = 1,
|
DASHBOARD = 1,
|
||||||
DEVICES = 2,
|
ROOM_LIST = 2,
|
||||||
DOOR = 4,
|
AGENT_MANAGEMENT = 3,
|
||||||
DOOR_LAYOUT = 5,
|
APP_MANAGEMENT = 4,
|
||||||
BUILDING_DASHBOARD = 6,
|
COMMAND_SENDER = 5,
|
||||||
SETUP_DOOR = 7,
|
BLACKLIST = 6,
|
||||||
DOOR_STATUS = 8,
|
ROOM_DETAIL = 7,
|
||||||
DEPARTMENTS = 9,
|
LIST_ROLES = 8,
|
||||||
DEPARTMENT_PATHS = 10,
|
LIST_PERMISSIONS = 9,
|
||||||
SCHEDULES = 11,
|
LIST_USERS = 10,
|
||||||
ACCESS_STATUS = 12,
|
|
||||||
ACCESS_HISTORY = 13,
|
|
||||||
CONFIG_MANAGER,
|
|
||||||
APP_VERSION_MANAGER,
|
|
||||||
DEVICES_APP_VERSION,
|
|
||||||
HEALTHCHEAK,
|
|
||||||
LIST_ROLES,
|
|
||||||
ACCOUNT_PERMISSION,
|
|
||||||
LIST_ACCOUNT,
|
|
||||||
DOOR_WARNING,
|
|
||||||
COMMAND_HISTORY,
|
|
||||||
ACCESS_ILLEGAL,
|
|
||||||
ZONES,
|
|
||||||
MANTRAP,
|
|
||||||
ROLES,
|
|
||||||
DEVICES_SYNC_BIO,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const appSidebarSection = {
|
export const appSidebarSection = {
|
||||||
versions: ["1.0.1", "1.1.0-alpha", "2.0.0-beta1"],
|
versions: ["1.0.1", "1.1.0-alpha", "2.0.0-beta1"],
|
||||||
navMain: [
|
navMain: [
|
||||||
{ title: "Dashboard", to: "/", icon: Home },
|
|
||||||
{
|
{
|
||||||
title: "Danh sách phòng",
|
title: "Thống kê tổng quan",
|
||||||
to: "/room",
|
items: [
|
||||||
icon: Building,
|
{
|
||||||
|
title: "Dashboard",
|
||||||
|
url: "/dashboard",
|
||||||
|
code: AppSidebarSectionCode.DASHBOARD,
|
||||||
|
icon: Home,
|
||||||
|
permissions: [PermissionEnum.ALLOW_ALL],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Quản lý Agent",
|
title: "Quan lý phòng máy",
|
||||||
to: "/agent",
|
items: [
|
||||||
|
{
|
||||||
|
title: "Danh sách phòng máy",
|
||||||
|
url: "/rooms",
|
||||||
|
code: AppSidebarSectionCode.ROOM_LIST,
|
||||||
|
icon: Building,
|
||||||
|
permissions: [PermissionEnum.VIEW_ROOM],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Agent và phần mềm",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "Danh sách Agent",
|
||||||
|
url: "/agent",
|
||||||
|
code: AppSidebarSectionCode.AGENT_MANAGEMENT,
|
||||||
icon: AppWindow,
|
icon: AppWindow,
|
||||||
|
permissions: [PermissionEnum.VIEW_AGENT],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Quản lý phần mềm",
|
title: "Quản lý phần mềm",
|
||||||
to: "/apps",
|
url: "/apps",
|
||||||
icon: AppWindow,
|
icon: AppWindow,
|
||||||
|
permissions: [PermissionEnum.VIEW_APPS],
|
||||||
|
}
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{ title: "Gửi lệnh CMD", to: "/command", icon: Terminal },
|
|
||||||
{
|
{
|
||||||
title: "Danh sách đen",
|
title: "Lệnh và các ứng dụng bị chặn",
|
||||||
to: "/blacklist",
|
items:
|
||||||
icon: CircleX,
|
[
|
||||||
|
{
|
||||||
|
title: "Gửi lệnh từ xa",
|
||||||
|
url: "/commands",
|
||||||
|
icon: Terminal,
|
||||||
|
permissions: [PermissionEnum.VIEW_COMMAND],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Danh sách ứng dụng/web bị chặn",
|
||||||
|
url: "/blacklists",
|
||||||
|
icon: CircleX,
|
||||||
|
permissions: [PermissionEnum.ALLOW_ALL],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Phân quyền và người dùng",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "Danh sách roles",
|
||||||
|
url: "/role",
|
||||||
|
icon: ShieldCheck,
|
||||||
|
permissions: [PermissionEnum.VIEW_ROLES],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
export type Permission = {
|
export type Permission = {
|
||||||
id: number;
|
id?: number; // From DB API
|
||||||
name: string;
|
name: string;
|
||||||
code: string;
|
code: string;
|
||||||
parentId: number | null;
|
value: number; // Enum value from API
|
||||||
enum: PermissionEnum;
|
parentId?: number | null;
|
||||||
|
enum?: PermissionEnum; // Deprecated, use value
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PermissionOnRole = {
|
export type PermissionOnRole = {
|
||||||
|
|
@ -29,31 +30,20 @@ export enum PermissionEnum {
|
||||||
EDIT_APP_CONFIG = 23,
|
EDIT_APP_CONFIG = 23,
|
||||||
DEL_APP_CONFIG = 24,
|
DEL_APP_CONFIG = 24,
|
||||||
|
|
||||||
//BIOMETRIC_OPERATION
|
//ROOM_OPERATION
|
||||||
BIOMETRIC_OPERATION = 30,
|
|
||||||
VIEW_GUEST = 31,
|
|
||||||
GET_BIO = 32,
|
|
||||||
GET_SEND_BIO_STATUS = 33,
|
|
||||||
|
|
||||||
//BUILDING_OPERATION
|
|
||||||
BUILDING_OPERATION = 40,
|
BUILDING_OPERATION = 40,
|
||||||
VIEW_BUILDING = 41,
|
VIEW_ROOM = 41,
|
||||||
CREATE_BUILDING = 42,
|
CREATE_ROOM = 42,
|
||||||
EDIT_BUILDING = 43,
|
EDIT_ROOM = 43,
|
||||||
CREATE_LV = 45,
|
DEL_ROOM = 44,
|
||||||
DEL_BUILDING = 44,
|
|
||||||
|
|
||||||
//COMMAND_OPERATION
|
//COMMAND_OPERATION
|
||||||
COMMAND_OPERATION = 50,
|
COMMAND_OPERATION = 50,
|
||||||
VIEW_COMMAND = 51,
|
VIEW_COMMAND = 51,
|
||||||
|
CREATE_COMMAND = 52,
|
||||||
//DEPARTMENT_OPERATION
|
EDIT_COMMAND = 53,
|
||||||
DEPARTMENT_OPERATION = 60,
|
DEL_COMMAND = 54,
|
||||||
VIEW_DEP = 61,
|
SEND_COMMAND = 55,
|
||||||
CREATE_DEP = 62,
|
|
||||||
EDIT_DEP = 63,
|
|
||||||
DEL_DEP = 64,
|
|
||||||
VIEW_PATH = 65,
|
|
||||||
|
|
||||||
//DEVICE_OPERATION
|
//DEVICE_OPERATION
|
||||||
DEVICE_OPERATION = 70,
|
DEVICE_OPERATION = 70,
|
||||||
|
|
@ -61,55 +51,13 @@ export enum PermissionEnum {
|
||||||
EDIT_DEVICE = 73,
|
EDIT_DEVICE = 73,
|
||||||
VIEW_DEVICE = 74,
|
VIEW_DEVICE = 74,
|
||||||
|
|
||||||
//DOOR_OPERATION
|
|
||||||
DOOR_OPERATION = 80,
|
|
||||||
SET_DOOR_POSITION = 85,
|
|
||||||
RESET_DOOR_POSITION = 86,
|
|
||||||
VIEW_DOOR = 81,
|
|
||||||
ADD_DOOR = 82,
|
|
||||||
EDIT_DOOR = 83,
|
|
||||||
DEL_DOOR = 84,
|
|
||||||
ADD_DEVICE_TO_DOOR = 87,
|
|
||||||
REMOVE_DEVICE_FROM_DOOR = 88,
|
|
||||||
|
|
||||||
SEND_COMMAND = 801,
|
|
||||||
SEND_EMERGENCY = 803,
|
|
||||||
CONTROL_DOOR = 805,
|
|
||||||
|
|
||||||
//LEVEL_OPERATION
|
|
||||||
LEVEL_OPERATION = 90,
|
|
||||||
UPLOAD_LAYOUT = 91,
|
|
||||||
VIEW_LEVEL_IN_BUILDING = 92,
|
|
||||||
EDIT_LV = 93,
|
|
||||||
DEL_LV = 94,
|
|
||||||
|
|
||||||
//PATH_OPERATION
|
|
||||||
PATH_OPERATION = 100,
|
|
||||||
CREATE_PATH = 102,
|
|
||||||
EDIT_PATH = 103,
|
|
||||||
DEL_PATH = 104,
|
|
||||||
|
|
||||||
//PERMISSION_OPERATION
|
//PERMISSION_OPERATION
|
||||||
PERMISSION_OPERATION = 110,
|
PERMISSION_OPERATION = 110,
|
||||||
VIEW_ALL_PER = 111,
|
VIEW_ALL_PER = 111,
|
||||||
CRE_PER = 112,
|
CRE_PER = 112,
|
||||||
DEL_PER = 114,
|
DEL_PER = 114,
|
||||||
VIEW_ACCOUNT_BUILDING = 115,
|
VIEW_ACCOUNT_ROOM = 115,
|
||||||
EDIT_ACCOUNT_BUILDING = 116,
|
EDIT_ACCOUNT_ROOM = 116,
|
||||||
|
|
||||||
//ZONE_OPERATION
|
|
||||||
ZONE_OPERATION = 120,
|
|
||||||
CREATE_ZONE = 122,
|
|
||||||
EDIT_ZONE = 123,
|
|
||||||
DEL_ZONE = 124,
|
|
||||||
VIEW_ZONE = 121,
|
|
||||||
|
|
||||||
//SCHEDULE_OPERATION
|
|
||||||
SCHEDULE_OPERATION = 130,
|
|
||||||
DEL_SCHEDULE = 134,
|
|
||||||
CREATE_SCHEDULE = 132,
|
|
||||||
EDIT_SCHEDULE = 133,
|
|
||||||
VIEW_ALL_SCHEDULE = 131,
|
|
||||||
|
|
||||||
//WARNING_OPERATION
|
//WARNING_OPERATION
|
||||||
WARNING_OPERATION = 140,
|
WARNING_OPERATION = 140,
|
||||||
|
|
@ -121,6 +69,7 @@ export enum PermissionEnum {
|
||||||
VIEW_USER = 152,
|
VIEW_USER = 152,
|
||||||
EDIT_USER_ROLE = 153,
|
EDIT_USER_ROLE = 153,
|
||||||
CRE_USER = 154,
|
CRE_USER = 154,
|
||||||
|
CHANGE_PASSWORD = 155,
|
||||||
|
|
||||||
//ROLE_OPERATION
|
//ROLE_OPERATION
|
||||||
ROLE_OPERATION = 160,
|
ROLE_OPERATION = 160,
|
||||||
|
|
@ -130,15 +79,24 @@ export enum PermissionEnum {
|
||||||
EDIT_ROLE_PER = 163,
|
EDIT_ROLE_PER = 163,
|
||||||
DEL_ROLE = 164,
|
DEL_ROLE = 164,
|
||||||
|
|
||||||
// APP VERSION
|
// AGENT
|
||||||
APP_VERSION_OPERATION = 170,
|
APP_OPERATION = 170,
|
||||||
VIEW_APP_VERSION = 171,
|
VIEW_AGENT = 171,
|
||||||
UPLOAD_APK = 172,
|
UPDATE_AGENT = 173,
|
||||||
|
SEND_UPDATE_COMMAND = 174,
|
||||||
|
|
||||||
CHANGE_PASSWORD = 2,
|
// APPS
|
||||||
|
APPS_OPERATION = 180,
|
||||||
|
VIEW_APPS = 181,
|
||||||
|
CREATE_APP = 182,
|
||||||
|
EDIT_APP = 183,
|
||||||
|
DEL_APP = 184,
|
||||||
|
ADD_APP_TO_SELECTED = 185,
|
||||||
|
DEL_APP_FROM_SELECTED = 186,
|
||||||
|
|
||||||
//Undefined
|
//Undefined
|
||||||
UNDEFINED = 9999,
|
UNDEFINED = 9999,
|
||||||
|
|
||||||
ALLOW_ALL = 0
|
//Allow All
|
||||||
|
ALLOW_ALL = 0,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
src/types/role.ts
Normal file
15
src/types/role.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
export type TCreateRoleRequestBody = {
|
||||||
|
RoleName: string;
|
||||||
|
Priority: number;
|
||||||
|
PermissionIds: number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TRoleResponse = {
|
||||||
|
id: number;
|
||||||
|
roleName: string;
|
||||||
|
priority: number;
|
||||||
|
createdAt: Date | null;
|
||||||
|
updatedAt: Date | null;
|
||||||
|
createdBy: string | null;
|
||||||
|
updatedBy: string | null;
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user