diff --git a/fe-admin/src/App.tsx b/fe-admin/src/App.tsx index 3212b79..4786701 100644 --- a/fe-admin/src/App.tsx +++ b/fe-admin/src/App.tsx @@ -10,6 +10,7 @@ import { ProjectsPage } from '@/pages/master/ProjectsPage' import { DepartmentsPage } from '@/pages/master/DepartmentsPage' import { CatalogsPage } from '@/pages/master/CatalogsPage' import { PermissionsPage } from '@/pages/system/PermissionsPage' +import { RolesPage } from '@/pages/system/RolesPage' import { WorkflowsPage } from '@/pages/system/WorkflowsPage' import { FormsPage } from '@/pages/forms/FormsPage' import { ContractsListPage } from '@/pages/contracts/ContractsListPage' @@ -38,6 +39,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/fe-admin/src/pages/system/RolesPage.tsx b/fe-admin/src/pages/system/RolesPage.tsx new file mode 100644 index 0000000..4831484 --- /dev/null +++ b/fe-admin/src/pages/system/RolesPage.tsx @@ -0,0 +1,268 @@ +// 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ã codeMã viết tắtTên đầy đủLoạiNgà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..." + /> +
+
+ +