From 072ad6d01481c22fe223c8707f203eb87c59ed92 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Thu, 23 Apr 2026 14:57:36 +0700 Subject: [PATCH] [CLAUDE] App+Api+FE-Admin: RolesPage CRUD (/system/roles) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User feedback: /system/roles trỏ tới placeholder "chưa được build" — build trang quản lý 12 role mặc định + custom role admin tự thêm. ## BE — PermissionFeatures.cs 3 command mới: - CreateRoleCommand — Name regex `^[A-Za-z][A-Za-z0-9_]*$` (chỉ chữ/số/ underscore, bắt đầu chữ), throw ConflictException nếu code đã tồn tại - UpdateRoleCommand — CHỈ update ShortName + Description. KHÔNG đổi Name (Identity FK trong UserRoles + WorkflowStepApprover.AssignmentValue + [Authorize(Roles="...")] attr — đổi = data corruption widespread) - DeleteRoleCommand — block 2 trường hợp: * Role thuộc AppRoles.All hardcoded (workflow guard reference) * Còn user assigned (UserManager.GetUsersInRoleAsync count > 0) ValidationException reference fully-qualified để tránh ambiguous với FluentValidation.ValidationException. ## BE — RolesController 3 endpoint mới (POST/PUT/DELETE) — Authorize Admin role. ## FE — RolesPage Table list 12 + custom roles với 5 column (Mã code / Mã viết tắt / Tên đầy đủ / Loại badge / Ngày tạo) + actions Edit/Delete: - Edit dialog: chỉ ShortName + Description editable, Name disabled với hint "Không đổi được sau khi tạo" - Delete: block với toast nếu role mặc định (HARDCODED_ROLES set check client-side trước khi gọi BE — UX faster, BE vẫn double-check) - Create dialog: 3 field Name (regex pattern HTML5) + ShortName + Description - Banner amber warning về Mã code FK constraint - Loại badge: Mặc định (slate) vs Tùy chỉnh (brand) ## FE — App.tsx + import RolesPage + route /system/roles → RolesPage. ## Build - BE: dotnet build pass (0 error) - fe-admin: tsc + vite pass (13.88s) Co-Authored-By: Claude Opus 4.7 (1M context) --- fe-admin/src/App.tsx | 2 + fe-admin/src/pages/system/RolesPage.tsx | 268 ++++++++++++++++++ .../Controllers/RolesController.cs | 25 ++ .../Permissions/PermissionFeatures.cs | 103 +++++++ 4 files changed, 398 insertions(+) create mode 100644 fe-admin/src/pages/system/RolesPage.tsx 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..." + /> +
+
+ +