// Quản lý 12 role mặc định + custom role admin tự thêm. Edit chỉ ShortName + // Description (Mã = Identity Name là FK + [Authorize] attr — không cho đổi). // Delete chỉ cho custom role chưa có user assigned (BE block 12 hardcoded). import { useState, type FormEvent } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { Pencil, Plus, Shield, Trash2, AlertCircle } from 'lucide-react' import { toast } from 'sonner' import { PageHeader } from '@/components/PageHeader' import { Button } from '@/components/ui/Button' import { Input } from '@/components/ui/Input' import { Label } from '@/components/ui/Label' import { Textarea } from '@/components/ui/Textarea' import { Dialog } from '@/components/ui/Dialog' import { api } from '@/lib/api' import { getErrorMessage } from '@/lib/apiError' import { AVAILABLE_ROLES } from '@/types/users' import type { Role } from '@/types/menu' const HARDCODED_ROLES = new Set(AVAILABLE_ROLES) const fmtDate = (s: string) => new Date(s).toLocaleDateString('vi-VN') export function RolesPage() { const qc = useQueryClient() const [createOpen, setCreateOpen] = useState(false) const [createForm, setCreateForm] = useState({ name: '', shortName: '', description: '' }) const [editTarget, setEditTarget] = useState(null) const [editForm, setEditForm] = useState({ shortName: '', description: '' }) const list = useQuery({ queryKey: ['roles'], queryFn: async () => (await api.get('/roles')).data, }) const createMut = useMutation({ mutationFn: async () => { await api.post('/roles', { name: createForm.name.trim(), shortName: createForm.shortName.trim() || null, description: createForm.description.trim() || null, }) }, onSuccess: () => { qc.invalidateQueries({ queryKey: ['roles'] }) toast.success('Đã tạo role') setCreateOpen(false) setCreateForm({ name: '', shortName: '', description: '' }) }, onError: err => toast.error(getErrorMessage(err)), }) const editMut = useMutation({ mutationFn: async () => { if (!editTarget) return await api.put(`/roles/${editTarget.id}`, { id: editTarget.id, shortName: editForm.shortName.trim() || null, description: editForm.description.trim() || null, }) }, onSuccess: () => { qc.invalidateQueries({ queryKey: ['roles'] }) toast.success('Đã lưu') setEditTarget(null) }, onError: err => toast.error(getErrorMessage(err)), }) const deleteMut = useMutation({ mutationFn: async (id: string) => { await api.delete(`/roles/${id}`) }, onSuccess: () => { qc.invalidateQueries({ queryKey: ['roles'] }) toast.success('Đã xóa role') }, onError: err => toast.error(getErrorMessage(err)), }) function openEdit(r: Role) { setEditTarget(r) setEditForm({ shortName: r.shortName ?? '', description: r.description ?? '' }) } return (
Vai trò (Roles) } description="12 role mặc định seed lúc startup + custom role admin thêm. Chỉ sửa Mã viết tắt + Tên đầy đủ; không đổi Mã code (FK)." actions={ } />
Mã code (Name) là khóa kỹ thuật — KHÔNG đổi sau khi tạo (tham chiếu UserRoles + WorkflowStepApprover + [Authorize]). Chỉ Mã viết tắt + Tên đầy đủ tiếng Việt được sửa.
{list.isLoading && } {!list.isLoading && (list.data?.length ?? 0) === 0 && ( )} {list.data?.map(r => { const isSystem = HARDCODED_ROLES.has(r.name) return ( ) })}
Mã code Mã viết tắt Tên đầy đủ Loại Ngày tạo
Đang tải…
Không có role.
{r.name} {r.shortName ?? '—'} {r.description ?? } {isSystem ? ( Mặc định ) : ( Tùy chỉnh )} {fmtDate(r.createdAt)}
{/* Create custom role */} setCreateOpen(false)} title="Thêm role tùy chỉnh" size="md" footer={ <> } >
{ e.preventDefault(); createMut.mutate() }}>
setCreateForm(f => ({ ...f, name: e.target.value }))} placeholder="Auditor, ITSupport, Reception..." required pattern="^[A-Za-z][A-Za-z0-9_]*$" />
Chỉ chữ + số + underscore, bắt đầu bằng chữ. Dùng cho [Authorize(Roles="...")] + workflow guard.
setCreateForm(f => ({ ...f, shortName: e.target.value }))} placeholder="vd: KSV, IT, LT..." />