GetMyMenuTreeQuery truoc chi inherit Contracts (Ct_*) va Workflows
(Wf_*). Extend 2 root moi PurchaseEvaluations (Pe_*) + PeWorkflows
(PeWf_*) de admin co PurchaseEvaluations.Read auto thay 2 group Pe_* +
6 leaf (Danh sach/Thao tac/Duyet x 2 type) + 2 PeWf_* leaf admin designer
UI, khong can add per-subitem permission row.
Verify bug: /menus/me cho admin hien chi root 'PurchaseEvaluations'
+ 'PeWorkflows' nhung khong co Pe_DuyetNcc group / Pe_DuyetNccPhuongAn
group children du DB co 12 row (sqlcmd confirm). Root cause: hardcoded
2 inherit roots trong BuildChildren switch.
Fix: expand switch cover 4 inherit roots. Propagate nextInherit xuong
tat ca descendants.
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) <noreply@anthropic.com>
User request: 7 tab trong /system/workflows thành menu items riêng.
Domain:
- MenuKeys.WorkflowTypeLeaf(code) helper — `Wf_<TypeCode>` pattern
Infrastructure (DbInitializer):
- Seed 7 leaves dưới Workflows group (order 95..101), label matches
ContractType (HĐ Thầu phụ / Giao khoán / NCC / Dịch vụ / Mua bán /
Nguyên tắc NCC / Nguyên tắc Dịch vụ). Idempotent.
Application (GetMyMenuTreeQuery):
- Generalized inherit-perm logic: descendants of Contracts AND Workflows
inherit parent CanRead flag. Single Workflows.Read grant → all 7
Wf_* leaves visible; no per-leaf permission rows needed.
FE Layout (admin):
- resolvePath: Wf_<Code> → /system/workflows/<code>. Ct_* still hidden
on admin side.
FE App.tsx:
- New route /system/workflows/:typeCode?
FE WorkflowsPage:
- Removed horizontal tab bar; type selection now comes từ URL param.
- Landing view (no param): 3-col grid card per type với active version
badge — so admin có visual overview khi click top-level Workflows
group without selecting a type.
- TYPE_CODE_TO_INT map drives URL→int conversion.
Result: click `Quy trình HĐ > HĐ Mua bán` trong sidebar → opens
/system/workflows/MuaBan directly với designer scoped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User request: mỗi loại HĐ có menu riêng với 3 action Danh sách /
Thao tác / Duyệt.
Sidebar giờ 3-level under "Hợp đồng":
Hợp đồng (group, expandable)
├── HĐ Thầu phụ (sub-group)
│ ├── Danh sách → /contracts?type=1
│ ├── Thao tác → /contracts/new?type=1
│ └── Duyệt → /contracts?type=1&pendingMe=1
├── HĐ Giao khoán (sub-group)
├── HĐ NCC / Dịch vụ / Mua bán / Nguyên tắc NCC / Nguyên tắc DV
└── ... (7 types × 4 = 28 new menu items)
BE:
- MenuKeys.cs: ContractTypeCodes array + helpers ContractTypeGroup/
List/Create/Pending → key format Ct_<TypeCode>[_<Action>]
- DbInitializer.SeedMenuTreeAsync: loop seeds 28 entries under Contracts
- GetMyMenuTreeQuery.BuildChildren: descendants of `Contracts` inherit
parent permission (avoid adding 28 rows to Permissions table per role)
FE:
- Layout.tsx recursive: MenuNodeRenderer dispatches group vs leaf by
depth; nested groups collapsed by default (top-level expanded).
Deeper levels get smaller padding/text + left border guide.
- Pattern-based resolvePath: Ct_<Type>_<Action> → URL with query.
- Contract type code → int map (matches Domain ContractType enum).
- ContractsListPage reads ?type + ?pendingMe, filters client-side.
Header title + description reflect active filter. "← Tất cả loại"
quick-reset button.
- ContractCreatePage new cho admin (copy từ fe-user), pre-select type
từ ?type URL param.
- App.tsx route /contracts/new → ContractCreatePage.
Pure navigation UX; no new permissions needed. Admin + any role with
Contracts.Read see full menu; leaves click-through to filtered views.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>