274 lines
9.6 KiB
TypeScript
274 lines
9.6 KiB
TypeScript
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>
|
|
);
|
|
}
|