[CLAUDE] PurchaseEvaluation: Mig 26 PeLevelOpinions V2 dynamic — Chunk A Domain + EF
Schema mới cho Section 5 "Ý kiến cấp duyệt" V2 dynamic theo ApprovalWorkflowsV2 (Mig 22-25). Thay thế Mig 15 cố định 4 box (V1). Entity `PurchaseEvaluationLevelOpinion : AuditableEntity`: - (PEId, ApprovalWorkflowLevelId) UNIQUE composite - Comment nvarchar(2000) — text ý kiến hoặc "(duyệt — không ý kiến)" placeholder (Q4 bonus) - SignedAt datetime2 (luôn có khi UPSERT từ ApproveV2Async) - SignedByUserId Guid (NV chính chủ HOẶC Admin override) - SignedByFullName nvarchar(200) — denorm tránh user bị xóa/đổi tên EF: FK Cascade Pe + Restrict Level. SignedByUserId KHÔNG nav (denorm only). Migration 26 `AddPeLevelOpinionsForV2`: 1 CREATE TABLE + 2 FK + 2 index. 3-file rule commit đủ (.cs + Designer + Snapshot). Apply LocalDB SolutionErp_Dev OK (Mig 25 + 26 catchup). Verify: dotnet build pass + dotnet test 81 pass (no regression). Chunk B kế tiếp: Service V2 hook UPSERT auto trong ApproveV2Async.
This commit is contained in:
@ -64,6 +64,8 @@ public interface IApplicationDbContext
|
|||||||
DbSet<PurchaseEvaluationCodeSequence> PurchaseEvaluationCodeSequences { get; }
|
DbSet<PurchaseEvaluationCodeSequence> PurchaseEvaluationCodeSequences { get; }
|
||||||
DbSet<PurchaseEvaluationDepartmentOpinion> PurchaseEvaluationDepartmentOpinions { get; }
|
DbSet<PurchaseEvaluationDepartmentOpinion> PurchaseEvaluationDepartmentOpinions { get; }
|
||||||
DbSet<PurchaseEvaluationDepartmentApproval> PurchaseEvaluationDepartmentApprovals { get; }
|
DbSet<PurchaseEvaluationDepartmentApproval> PurchaseEvaluationDepartmentApprovals { get; }
|
||||||
|
// Mig 26 (Session 19) — Ý kiến cấp duyệt V2 dynamic theo ApprovalWorkflowLevel
|
||||||
|
DbSet<PurchaseEvaluationLevelOpinion> PurchaseEvaluationLevelOpinions { get; }
|
||||||
|
|
||||||
// Quy trình duyệt MỚI (Mig 22 — Session 17): schema riêng UAT trước khi
|
// Quy trình duyệt MỚI (Mig 22 — Session 17): schema riêng UAT trước khi
|
||||||
// drop legacy WorkflowDefinition. Cấu trúc: Quy trình > Bước (Phòng) > Cấp (NV cụ thể).
|
// drop legacy WorkflowDefinition. Cấu trúc: Quy trình > Bước (Phòng) > Cấp (NV cụ thể).
|
||||||
|
|||||||
@ -62,4 +62,9 @@ public class PurchaseEvaluation : AuditableEntity
|
|||||||
public List<PurchaseEvaluationAttachment> Attachments { get; set; } = new();
|
public List<PurchaseEvaluationAttachment> Attachments { get; set; } = new();
|
||||||
public List<PurchaseEvaluationDepartmentOpinion> DepartmentOpinions { get; set; } = new();
|
public List<PurchaseEvaluationDepartmentOpinion> DepartmentOpinions { get; set; } = new();
|
||||||
public List<PurchaseEvaluationDepartmentApproval> DepartmentApprovals { get; set; } = new();
|
public List<PurchaseEvaluationDepartmentApproval> DepartmentApprovals { get; set; } = new();
|
||||||
|
|
||||||
|
// Mig 26 (Session 19) — Ý kiến cấp duyệt V2 dynamic. UPSERT auto từ
|
||||||
|
// ApproveV2Async theo Cấp hiện tại. Section 5 FE render dynamic theo
|
||||||
|
// flow.steps[].levels[]. Phiếu V1 (WorkflowDefinitionId) KHÔNG dùng.
|
||||||
|
public List<PurchaseEvaluationLevelOpinion> LevelOpinions { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,36 @@
|
|||||||
|
using SolutionErp.Domain.ApprovalWorkflowsV2;
|
||||||
|
using SolutionErp.Domain.Common;
|
||||||
|
|
||||||
|
namespace SolutionErp.Domain.PurchaseEvaluations;
|
||||||
|
|
||||||
|
// "Ý kiến cấp duyệt" V2 — sign-off DYNAMIC theo workflow ApprovalWorkflowV2
|
||||||
|
// (Mig 22-25). Thay thế `PurchaseEvaluationDepartmentOpinion` (Mig 15) cố
|
||||||
|
// định 4 box (PheDuyet/Ccm/MuaHang/SmPm) cho phiếu V1.
|
||||||
|
//
|
||||||
|
// Mỗi row = 1 (PE × ApprovalWorkflowLevel). Service `ApproveV2Async` sau khi
|
||||||
|
// approve thành công Cấp hiện tại sẽ UPSERT row này:
|
||||||
|
// Comment = approval.Comment ?? "(duyệt — không ý kiến)" (Q4 bonus)
|
||||||
|
// SignedAt = clock.UtcNow
|
||||||
|
// SignedByUserId = actor.Id (NV chính chủ HOẶC Admin override)
|
||||||
|
// SignedByFullName = actor.FullName (denorm — tránh user bị xóa/đổi tên)
|
||||||
|
//
|
||||||
|
// Reject (Trả lại / Từ chối) KHÔNG sync (vì không phải sign-off của level đó).
|
||||||
|
// Khi user resubmit từ TraLai → workflow chạy lại từ Cấp 1, opinion cũ bị
|
||||||
|
// OVERWRITE bằng UPSERT mới (latest-write-wins).
|
||||||
|
//
|
||||||
|
// Section 5 FE detect V2 qua `pe.approvalWorkflowId != null` → render dynamic
|
||||||
|
// theo flow.steps[].levels[]. Phiếu V1 (WorkflowDefinitionId set) → fallback
|
||||||
|
// message "phiếu cũ không có ý kiến dynamic" (Q3 chốt: chuyển V2 hết).
|
||||||
|
public class PurchaseEvaluationLevelOpinion : AuditableEntity
|
||||||
|
{
|
||||||
|
public Guid PurchaseEvaluationId { get; set; }
|
||||||
|
public Guid ApprovalWorkflowLevelId { get; set; }
|
||||||
|
|
||||||
|
public string? Comment { get; set; } // ý kiến (max 2000) hoặc placeholder "(duyệt — không ý kiến)"
|
||||||
|
public DateTime SignedAt { get; set; } // luôn có khi UPSERT (Service set khi Approve)
|
||||||
|
public Guid SignedByUserId { get; set; } // người ký thực sự (có thể là Admin thay NV)
|
||||||
|
public string SignedByFullName { get; set; } = string.Empty; // snapshot tên — denorm
|
||||||
|
|
||||||
|
public PurchaseEvaluation? PurchaseEvaluation { get; set; }
|
||||||
|
public ApprovalWorkflowLevel? Level { get; set; }
|
||||||
|
}
|
||||||
@ -63,6 +63,8 @@ public class ApplicationDbContext
|
|||||||
public DbSet<PurchaseEvaluationCodeSequence> PurchaseEvaluationCodeSequences => Set<PurchaseEvaluationCodeSequence>();
|
public DbSet<PurchaseEvaluationCodeSequence> PurchaseEvaluationCodeSequences => Set<PurchaseEvaluationCodeSequence>();
|
||||||
public DbSet<PurchaseEvaluationDepartmentOpinion> PurchaseEvaluationDepartmentOpinions => Set<PurchaseEvaluationDepartmentOpinion>();
|
public DbSet<PurchaseEvaluationDepartmentOpinion> PurchaseEvaluationDepartmentOpinions => Set<PurchaseEvaluationDepartmentOpinion>();
|
||||||
public DbSet<PurchaseEvaluationDepartmentApproval> PurchaseEvaluationDepartmentApprovals => Set<PurchaseEvaluationDepartmentApproval>();
|
public DbSet<PurchaseEvaluationDepartmentApproval> PurchaseEvaluationDepartmentApprovals => Set<PurchaseEvaluationDepartmentApproval>();
|
||||||
|
// Mig 26 (Session 19) — Ý kiến cấp duyệt V2 dynamic
|
||||||
|
public DbSet<PurchaseEvaluationLevelOpinion> PurchaseEvaluationLevelOpinions => Set<PurchaseEvaluationLevelOpinion>();
|
||||||
|
|
||||||
// Quy trình duyệt mới (Mig 22 — Session 17): schema riêng UAT.
|
// Quy trình duyệt mới (Mig 22 — Session 17): schema riêng UAT.
|
||||||
public DbSet<ApprovalWorkflow> ApprovalWorkflows => Set<ApprovalWorkflow>();
|
public DbSet<ApprovalWorkflow> ApprovalWorkflows => Set<ApprovalWorkflow>();
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using SolutionErp.Domain.PurchaseEvaluations;
|
||||||
|
|
||||||
|
namespace SolutionErp.Infrastructure.Persistence.Configurations;
|
||||||
|
|
||||||
|
// EF Mig 26 — UPSERT auto sync từ ApproveV2Async. UNIQUE (PEId, LevelId)
|
||||||
|
// đảm bảo 1 row/level/phiếu. FK Cascade Pe (xoá phiếu → xoá opinions),
|
||||||
|
// FK Restrict Level (admin xoá Level chặn nếu opinion tồn tại — bảo vệ data).
|
||||||
|
// SignedByUserId KHÔNG nav (tránh cascade khi xoá user; denorm SignedByFullName).
|
||||||
|
public class PurchaseEvaluationLevelOpinionConfiguration : IEntityTypeConfiguration<PurchaseEvaluationLevelOpinion>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<PurchaseEvaluationLevelOpinion> e)
|
||||||
|
{
|
||||||
|
e.ToTable("PurchaseEvaluationLevelOpinions");
|
||||||
|
|
||||||
|
e.Property(x => x.Comment).HasMaxLength(2000);
|
||||||
|
e.Property(x => x.SignedByFullName).HasMaxLength(200).IsRequired();
|
||||||
|
|
||||||
|
e.HasOne(x => x.PurchaseEvaluation)
|
||||||
|
.WithMany(p => p.LevelOpinions)
|
||||||
|
.HasForeignKey(x => x.PurchaseEvaluationId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
e.HasOne(x => x.Level)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.ApprovalWorkflowLevelId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
e.HasIndex(x => new { x.PurchaseEvaluationId, x.ApprovalWorkflowLevelId }).IsUnique();
|
||||||
|
e.HasIndex(x => x.ApprovalWorkflowLevelId);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,69 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddPeLevelOpinionsForV2 : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PurchaseEvaluationLevelOpinions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
PurchaseEvaluationId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
ApprovalWorkflowLevelId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
Comment = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true),
|
||||||
|
SignedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
SignedByUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
SignedByFullName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, 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),
|
||||||
|
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
DeletedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PurchaseEvaluationLevelOpinions", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_PurchaseEvaluationLevelOpinions_ApprovalWorkflowLevels_ApprovalWorkflowLevelId",
|
||||||
|
column: x => x.ApprovalWorkflowLevelId,
|
||||||
|
principalTable: "ApprovalWorkflowLevels",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_PurchaseEvaluationLevelOpinions_PurchaseEvaluations_PurchaseEvaluationId",
|
||||||
|
column: x => x.PurchaseEvaluationId,
|
||||||
|
principalTable: "PurchaseEvaluations",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PurchaseEvaluationLevelOpinions_ApprovalWorkflowLevelId",
|
||||||
|
table: "PurchaseEvaluationLevelOpinions",
|
||||||
|
column: "ApprovalWorkflowLevelId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PurchaseEvaluationLevelOpinions_PurchaseEvaluationId_ApprovalWorkflowLevelId",
|
||||||
|
table: "PurchaseEvaluationLevelOpinions",
|
||||||
|
columns: new[] { "PurchaseEvaluationId", "ApprovalWorkflowLevelId" },
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PurchaseEvaluationLevelOpinions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2990,6 +2990,64 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
b.ToTable("PurchaseEvaluationDetails", (string)null);
|
b.ToTable("PurchaseEvaluationDetails", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationLevelOpinion", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid>("ApprovalWorkflowLevelId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("Comment")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("nvarchar(2000)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid?>("CreatedBy")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid?>("DeletedBy")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<Guid>("PurchaseEvaluationId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime>("SignedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("SignedByFullName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)");
|
||||||
|
|
||||||
|
b.Property<Guid>("SignedByUserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UpdatedBy")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ApprovalWorkflowLevelId");
|
||||||
|
|
||||||
|
b.HasIndex("PurchaseEvaluationId", "ApprovalWorkflowLevelId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("PurchaseEvaluationLevelOpinions", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationQuote", b =>
|
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationQuote", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@ -3647,6 +3705,25 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
b.Navigation("PurchaseEvaluation");
|
b.Navigation("PurchaseEvaluation");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationLevelOpinion", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ApprovalWorkflowLevelId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation")
|
||||||
|
.WithMany("LevelOpinions")
|
||||||
|
.HasForeignKey("PurchaseEvaluationId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Level");
|
||||||
|
|
||||||
|
b.Navigation("PurchaseEvaluation");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationQuote", b =>
|
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationQuote", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", "Detail")
|
b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", "Detail")
|
||||||
@ -3787,6 +3864,8 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
|
|
||||||
b.Navigation("Details");
|
b.Navigation("Details");
|
||||||
|
|
||||||
|
b.Navigation("LevelOpinions");
|
||||||
|
|
||||||
b.Navigation("Quotes");
|
b.Navigation("Quotes");
|
||||||
|
|
||||||
b.Navigation("Suppliers");
|
b.Navigation("Suppliers");
|
||||||
|
|||||||
Reference in New Issue
Block a user