[CLAUDE] Infra: phân cấp phòng ban (Department.ParentId) + /tree + gán phòng cha

Nền cây tổ chức cho trang Hồ sơ Nhân sự (anh: bố trí giống NamGroup; chốt phân
cấp thật + tự gán phòng cha trong quản lý phòng ban).
- Mig AddDepartmentParentId: +ParentId Guid? loose-Guid (no FK vật lý, convention
  PE) + IX_Departments_ParentId. AddColumn+CreateIndex, no new table, Down reversible
  (DropIndex->DropColumn). Chưa apply (CI/prod seed apply).
- GET /api/departments/tree: ráp cây in-memory + đếm NV active theo User.DepartmentId
  (EmployeeProfile KHÔNG có DepartmentId — link qua User, field phòng ban ở User Mig 11)
  + rollup TotalEmployeeCount + cycle-guard HashSet. Authz = class [Authorize] (= GET list).
- Create/Update +ParentId (write vẫn khóa Admin,CatalogManager). Update có cycle-guard:
  chặn tự-làm-cha + đi ngược chuỗi cha gặp lại Id (chống vòng A->B->A).
Build PASS (0 warn/err full solution). Test-after (test-specialist).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-16 10:34:52 +07:00
parent c98030f27c
commit 0f44d9754d
7 changed files with 6370 additions and 4 deletions

View File

@ -18,6 +18,12 @@ public class DepartmentsController(IMediator mediator) : ControllerBase
CancellationToken ct = default)
=> Ok(await mediator.Send(new ListDepartmentsQuery { Page = page, PageSize = pageSize, Search = search, SortBy = sortBy, SortDesc = sortDesc }, ct));
// Cây tổ chức phân cấp phòng ban (nền trang Hồ sơ Nhân sự). Authz = giống
// [HttpGet] List ở trên (chỉ class-level [Authorize], không attribute per-action).
[HttpGet("tree")]
public async Task<ActionResult<List<DepartmentTreeNodeDto>>> Tree(CancellationToken ct)
=> Ok(await mediator.Send(new GetDepartmentTreeQuery(), ct));
[HttpGet("{id:guid}")]
public async Task<ActionResult<DepartmentDto>> Get(Guid id, CancellationToken ct)
=> Ok(await mediator.Send(new GetDepartmentQuery(id), ct));