[CLAUDE] Domain+Infra+App+Api+FE-Admin: versioned workflow per ContractType
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Failing after 1m32s

User yêu cầu: mỗi loại HĐ có quy trình riêng với admin add roles + users
vào từng bước. Khi tạo version mới → HĐ tương lai chạy theo, HĐ cũ giữ
version cũ.

Domain:
- WorkflowDefinition (Code + Version + ContractType + IsActive + Steps)
- WorkflowStep (Order + Phase + Name + SlaDays + Approvers)
- WorkflowStepApprover (Kind: Role/User + AssignmentValue)
- Contract.WorkflowDefinitionId — pinned at creation
- WorkflowPolicyRegistry.FromDefinition() — build runtime policy từ DB

Infrastructure:
- EF config + migration AddVersionedWorkflows (3 table mới)
- DbInitializer.SeedWorkflowDefinitionsAsync: v01 per 7 ContractType,
  steps sinh từ hardcoded WorkflowPolicies (Role approvers).
- ContractWorkflowService.TransitionAsync: load pinned WorkflowDefinition
  → FromDefinition(), fallback cho HĐ cũ không có pin.

Application:
- CreateContractCommand pin WorkflowDefinitionId = active version cho type
- ContractFeatures.Get(id): load pinned def cho workflow summary
- WorkflowAdminFeatures: GetWorkflowAdminOverviewQuery (7 types + active
  + history + ContractsUsingCount), CreateWorkflowDefinitionCommand
  (validate payload, auto-increment version, deactivate old).

Api:
- GET /api/workflows trả overview
- POST /api/workflows tạo version mới (deactivate old)

FE /system/workflows:
- Tabs per 7 ContractType, mỗi tab hiện active version + lịch sử
- DefinitionCard: steps với badge role/user + SLA + archived indicator
  hiện "N HĐ còn chạy" cho version cũ
- WorkflowDesigner modal: form code/name/desc + danh sách steps
  (phase/name/SLA) + approvers (+ Role hoặc + User). Drop step ok.
  Clone từ version hiện tại để tạo v02 có điểm start sensible.
- Amber banner: HĐ cũ không bị ảnh hưởng khi tạo version mới

Invariants được giữ:
- Unique (Code, Version) index
- Chỉ 1 version IsActive per ContractType tại 1 thời điểm
- Set default sẽ auto xóa override → respect legacy override table
- Role-kind approvers drive transition guards; User-kind fallback
  DeptManager role cho v1 (user-level targeting = iteration 2)

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

View File

@ -0,0 +1,131 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SolutionErp.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddVersionedWorkflows : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "WorkflowDefinitionId",
table: "Contracts",
type: "uniqueidentifier",
nullable: true);
migrationBuilder.CreateTable(
name: "WorkflowDefinitions",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Code = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
Version = table.Column<int>(type: "int", nullable: false),
ContractType = table.Column<int>(type: "int", nullable: false),
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
Description = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false),
ActivatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
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_WorkflowDefinitions", x => x.Id);
});
migrationBuilder.CreateTable(
name: "WorkflowSteps",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
WorkflowDefinitionId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Order = table.Column<int>(type: "int", nullable: false),
Phase = table.Column<int>(type: "int", nullable: false),
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
SlaDays = table.Column<int>(type: "int", nullable: true),
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_WorkflowSteps", x => x.Id);
table.ForeignKey(
name: "FK_WorkflowSteps_WorkflowDefinitions_WorkflowDefinitionId",
column: x => x.WorkflowDefinitionId,
principalTable: "WorkflowDefinitions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "WorkflowStepApprovers",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
WorkflowStepId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Kind = table.Column<int>(type: "int", nullable: false),
AssignmentValue = table.Column<string>(type: "nvarchar(100)", maxLength: 100, 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_WorkflowStepApprovers", x => x.Id);
table.ForeignKey(
name: "FK_WorkflowStepApprovers_WorkflowSteps_WorkflowStepId",
column: x => x.WorkflowStepId,
principalTable: "WorkflowSteps",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_WorkflowDefinitions_Code_Version",
table: "WorkflowDefinitions",
columns: new[] { "Code", "Version" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_WorkflowDefinitions_ContractType_IsActive",
table: "WorkflowDefinitions",
columns: new[] { "ContractType", "IsActive" });
migrationBuilder.CreateIndex(
name: "IX_WorkflowStepApprovers_WorkflowStepId",
table: "WorkflowStepApprovers",
column: "WorkflowStepId");
migrationBuilder.CreateIndex(
name: "IX_WorkflowSteps_WorkflowDefinitionId_Order",
table: "WorkflowSteps",
columns: new[] { "WorkflowDefinitionId", "Order" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "WorkflowStepApprovers");
migrationBuilder.DropTable(
name: "WorkflowSteps");
migrationBuilder.DropTable(
name: "WorkflowDefinitions");
migrationBuilder.DropColumn(
name: "WorkflowDefinitionId",
table: "Contracts");
}
}
}

View File

@ -201,6 +201,9 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("WorkflowDefinitionId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("MaHopDong")
@ -374,6 +377,138 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.ToTable("ContractComments", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowDefinition", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime?>("ActivatedAt")
.HasColumnType("datetime2");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("ContractType")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<string>("Description")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("Code", "Version")
.IsUnique();
b.HasIndex("ContractType", "IsActive");
b.ToTable("WorkflowDefinitions", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStep", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<int>("Order")
.HasColumnType("int");
b.Property<int>("Phase")
.HasColumnType("int");
b.Property<int?>("SlaDays")
.HasColumnType("int");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("WorkflowDefinitionId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("WorkflowDefinitionId", "Order");
b.ToTable("WorkflowSteps", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStepApprover", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("AssignmentValue")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<int>("Kind")
.HasColumnType("int");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("WorkflowStepId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("WorkflowStepId");
b.ToTable("WorkflowStepApprovers", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowTypeAssignment", b =>
{
b.Property<Guid>("Id")
@ -1049,6 +1184,28 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.Navigation("Contract");
});
modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStep", b =>
{
b.HasOne("SolutionErp.Domain.Contracts.WorkflowDefinition", "WorkflowDefinition")
.WithMany("Steps")
.HasForeignKey("WorkflowDefinitionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("WorkflowDefinition");
});
modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStepApprover", b =>
{
b.HasOne("SolutionErp.Domain.Contracts.WorkflowStep", "Step")
.WithMany("Approvers")
.HasForeignKey("WorkflowStepId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Step");
});
modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b =>
{
b.HasOne("SolutionErp.Domain.Identity.MenuItem", "Parent")
@ -1087,6 +1244,16 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.Navigation("Comments");
});
modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowDefinition", b =>
{
b.Navigation("Steps");
});
modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStep", b =>
{
b.Navigation("Approvers");
});
modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b =>
{
b.Navigation("Children");