[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

@ -16,6 +16,7 @@ public class DepartmentConfiguration : IEntityTypeConfiguration<Department>
b.Property(x => x.Note).HasMaxLength(1000);
b.HasIndex(x => x.Code).IsUnique().HasFilter("[IsDeleted] = 0"); // Mig 47 (gotcha #57 EXT) — soft-deleted slot reusable, khớp HasQueryFilter !IsDeleted app-check
b.HasIndex(x => x.ParentId); // cây tổ chức — loose-Guid KHÔNG HasOne self-FK (convention loose-Guid hệ này)
b.HasQueryFilter(x => !x.IsDeleted);
}

View File

@ -0,0 +1,38 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SolutionErp.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddDepartmentParentId : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "ParentId",
table: "Departments",
type: "uniqueidentifier",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Departments_ParentId",
table: "Departments",
column: "ParentId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Departments_ParentId",
table: "Departments");
migrationBuilder.DropColumn(
name: "ParentId",
table: "Departments");
}
}
}

View File

@ -3101,6 +3101,9 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<Guid?>("ParentId")
.HasColumnType("uniqueidentifier");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
@ -3113,6 +3116,8 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
.IsUnique()
.HasFilter("[IsDeleted] = 0");
b.HasIndex("ParentId");
b.ToTable("Departments", (string)null);
});