[CLAUDE] Move nested-type menu → fe-user; Admin workflow config page
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m41s

User clarified: menu loại HĐ 3-level (Danh sách/Thao tác/Duyệt) thuộc
fe-user. Admin có page riêng để config quy trình per loại HĐ.

fe-admin Layout:
- filterForAdmin() drops Ct_* entries (hide nested type menu).
- Admin sidebar giờ về lại đơn giản: Dashboard / Master / Hợp đồng
  (leaf) / Forms / Reports / System.

fe-user Layout:
- Dynamic menu tree từ /menus/me (thay fixed USER_MENU hardcoded).
- Recursive MenuNodeRenderer (top-level expanded, nested collapsed).
- resolvePath user-specific: Ct_*_List → /my-contracts?type=X,
  Ct_*_Create → /contracts/new?type=X, Ct_*_Pending → /inbox?type=X.
- filterForUser drops admin-only entries (Master/System/Forms/Reports).
- Static USER_FIXED_TOP prepends "Hộp thư" leaf → /inbox.
- MyContractsPage + InboxPage đọc ?type=X param, filter client-side.

Workflow config (Admin side):
- Domain: WorkflowTypeAssignment entity (ContractType → PolicyName
  override). Registry.ForContractWithOverrides() prefer DB override
  else default.
- Infrastructure: EF config + migration AddWorkflowTypeAssignments,
  unique index trên ContractType. ContractWorkflowService load
  overrides dict mỗi transition. ContractFeatures load overrides khi
  build WorkflowSummaryDto.
- Application: GetWorkflowAdminOverviewQuery returns 7 types × current
  policy + available policies. SetWorkflowAssignmentCommand validate
  policy name tồn tại; nếu = default thì delete override (no stale row).
- Api: GET /api/workflows + PUT /api/workflows/{contractType}
  với policy "Workflows.Read" + "Workflows.Update".
- Menu: new key `Workflows` dưới System, label "Quy trình HĐ".
- FE /system/workflows: 7 card per type, dropdown Standard/SkipCcm +
  'Đã override' badge khi khác default, phase sequence timeline,
  explanation banner ở top. Iteration 2 note: admin-authored custom
  policies.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-04-21 22:41:05 +07:00
parent 48e91fe7ca
commit 5e0f3801a1
20 changed files with 1737 additions and 48 deletions

View File

@ -27,6 +27,7 @@ public class ApplicationDbContext
public DbSet<ContractAttachment> ContractAttachments => Set<ContractAttachment>();
public DbSet<ContractCodeSequence> ContractCodeSequences => Set<ContractCodeSequence>();
public DbSet<Notification> Notifications => Set<Notification>();
public DbSet<WorkflowTypeAssignment> WorkflowTypeAssignments => Set<WorkflowTypeAssignment>();
protected override void OnModelCreating(ModelBuilder builder)
{

View File

@ -0,0 +1,17 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SolutionErp.Domain.Contracts;
namespace SolutionErp.Infrastructure.Persistence.Configurations;
public class WorkflowTypeAssignmentConfiguration : IEntityTypeConfiguration<WorkflowTypeAssignment>
{
public void Configure(EntityTypeBuilder<WorkflowTypeAssignment> e)
{
e.ToTable("WorkflowTypeAssignments");
e.HasKey(x => x.Id);
e.Property(x => x.ContractType).HasConversion<int>();
e.Property(x => x.PolicyName).HasMaxLength(50).IsRequired();
e.HasIndex(x => x.ContractType).IsUnique();
}
}

View File

@ -113,6 +113,7 @@ public static class DbInitializer
(MenuKeys.Users, "Người dùng", MenuKeys.System, 91, "User"),
(MenuKeys.Roles, "Vai trò", MenuKeys.System, 92, "Shield"),
(MenuKeys.Permissions, "Phân quyền", MenuKeys.System, 93, "KeyRound"),
(MenuKeys.Workflows, "Quy trình HĐ", MenuKeys.System, 94, "GitBranch"),
};
// Per-type sub-menu under Contracts: 1 group + 3 leaves each

View File

@ -0,0 +1,45 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SolutionErp.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddWorkflowTypeAssignments : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "WorkflowTypeAssignments",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
ContractType = table.Column<int>(type: "int", nullable: false),
PolicyName = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
UpdatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_WorkflowTypeAssignments", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_WorkflowTypeAssignments_ContractType",
table: "WorkflowTypeAssignments",
column: "ContractType",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "WorkflowTypeAssignments");
}
}
}

View File

@ -374,6 +374,40 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.ToTable("ContractComments", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowTypeAssignment", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<int>("ContractType")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<string>("PolicyName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("ContractType")
.IsUnique();
b.ToTable("WorkflowTypeAssignments", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Forms.ContractClause", b =>
{
b.Property<Guid>("Id")

View File

@ -36,7 +36,11 @@ public class ContractWorkflowService(
if (contract.Phase == targetPhase)
throw new ConflictException("HĐ đã ở phase đích.");
var policy = WorkflowPolicyRegistry.ForContract(contract);
// Admin may override the default policy per ContractType via the
// /system/workflows page. Load all overrides once (7 rows max).
var overrides = await db.WorkflowTypeAssignments.AsNoTracking()
.ToDictionaryAsync(a => a.ContractType, a => a.PolicyName, ct);
var policy = WorkflowPolicyRegistry.ForContractWithOverrides(contract, overrides);
var isAdmin = actorRoles.Contains(AppRoles.Admin);
var isSystem = actorUserId is null && decision == ApprovalDecision.AutoApprove;