diff --git a/fe-admin/src/pages/system/PermissionsPage.tsx b/fe-admin/src/pages/system/PermissionsPage.tsx index a8bb590..ec60cee 100644 --- a/fe-admin/src/pages/system/PermissionsPage.tsx +++ b/fe-admin/src/pages/system/PermissionsPage.tsx @@ -1,23 +1,27 @@ import { useMemo, useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' +import { Search, Shield, Check } from 'lucide-react' import { PageHeader } from '@/components/PageHeader' +import { EmptyState } from '@/components/EmptyState' +import { Input } from '@/components/ui/Input' import { Select } from '@/components/ui/Select' import { api } from '@/lib/api' import { getErrorMessage } from '@/lib/apiError' import type { MenuItem, Permission, Role } from '@/types/menu' type CrudKey = 'canRead' | 'canCreate' | 'canUpdate' | 'canDelete' -const CRUD_COLS: { key: CrudKey; label: string }[] = [ - { key: 'canRead', label: 'Xem' }, - { key: 'canCreate', label: 'Tạo' }, - { key: 'canUpdate', label: 'Sửa' }, - { key: 'canDelete', label: 'Xóa' }, +const CRUD_COLS: { key: CrudKey; label: string; short: string }[] = [ + { key: 'canRead', label: 'Xem', short: 'R' }, + { key: 'canCreate', label: 'Tạo', short: 'C' }, + { key: 'canUpdate', label: 'Sửa', short: 'U' }, + { key: 'canDelete', label: 'Xóa', short: 'D' }, ] export function PermissionsPage() { const qc = useQueryClient() const [roleId, setRoleId] = useState('') + const [search, setSearch] = useState('') const roles = useQuery({ queryKey: ['roles'], @@ -51,6 +55,27 @@ export function PermissionsPage() { return map }, [perms.data]) + const filteredMenus = useMemo(() => { + const all = menus.data ?? [] + if (!search.trim()) return all + const q = search.toLowerCase() + return all.filter(m => m.label.toLowerCase().includes(q) || m.key.toLowerCase().includes(q)) + }, [menus.data, search]) + + const stats = useMemo(() => { + const total = (menus.data?.length ?? 0) * 4 + let granted = 0 + for (const m of menus.data ?? []) { + const p = permMap.get(m.key) + if (!p) continue + if (p.canRead) granted++ + if (p.canCreate) granted++ + if (p.canUpdate) granted++ + if (p.canDelete) granted++ + } + return { granted, total } + }, [menus.data, permMap]) + function currentFlags(menuKey: string) { const p = permMap.get(menuKey) return { @@ -67,41 +92,123 @@ export function PermissionsPage() { upsert.mutate({ menuKey, ...next }) } + function columnAllChecked(field: CrudKey) { + return filteredMenus.length > 0 && filteredMenus.every(m => currentFlags(m.key)[field]) + } + + function toggleColumn(field: CrudKey) { + const allChecked = columnAllChecked(field) + const nextVal = !allChecked + for (const m of filteredMenus) { + const flags = currentFlags(m.key) + if (flags[field] !== nextVal) { + upsert.mutate({ menuKey: m.key, ...flags, [field]: nextVal }) + } + } + } + + const selectedRole = roles.data?.find(r => r.id === roleId) + return (
-
- +
+
+ + +
+
+ +
+ + setSearch(e.target.value)} + className="pl-8" + disabled={!roleId} + /> +
+
+ {roleId && ( +
+
+
+ + {selectedRole?.name} +
+
+ {stats.granted} + / {stats.total} quyền +
+
+
+ )}
- {roleId && ( + {!roleId ? ( + + ) : (
- - {CRUD_COLS.map(c => ( - - ))} + + {CRUD_COLS.map(c => { + const allChecked = columnAllChecked(c.key) + return ( + + ) + })} - {menus.data?.map(m => { + {filteredMenus.length === 0 && ( + + + + )} + {filteredMenus.map(m => { const flags = currentFlags(m.key) const depth = m.parentKey ? 1 : 0 return ( - +
Menu{c.label} + Menu + {search && ( + + ({filteredMenus.length} kết quả) + + )} + +
+ {c.label} + +
+
+ Không có menu khớp với từ khóa. +
{m.label} {m.key}