[CLAUDE] PurchaseEvaluation: User chọn quy trình duyệt V2 lúc tạo phiếu (Mig 23)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m11s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m11s
User feedback: thay field "Loại quy trình (theo menu — khóa)" disabled
→ Select dropdown cho User pick quy trình ApprovalWorkflowsV2 (Mig 22)
ngay từ workspace tạo mới. Hiển thị "Mã + Tên + Version".
BE Domain:
- PurchaseEvaluation +ApprovalWorkflowId Guid? (nullable, FK Restrict)
- EF Configuration: Index + FK Restrict to ApprovalWorkflows
- Migration 23 `AddApprovalWorkflowIdToPurchaseEvaluation` (1 ALTER +
1 IX + 1 FK), applied cả _Design + _Dev LocalDB
- Field WorkflowDefinitionId (Mig 21 legacy) giữ song song để Service
PE chạy logic cũ tới khi Session sau wire qua schema mới
BE Application:
- CreatePurchaseEvaluationCommand +ApprovalWorkflowId? Guid? optional
param (default null)
- Validate: nếu set, phải tồn tại + ApplicableType khớp PE.Type
(DuyetNcc=1 → ApprovalWorkflowApplicableType.DuyetNcc, etc)
- Handler set entity.ApprovalWorkflowId từ request
- UpdatePurchaseEvaluationDraftCommand mirror — cho User đổi quy trình
khi sửa Nháp/Trả lại (validate same)
- PurchaseEvaluationDetailBundleDto +ApprovalWorkflowId/Code/Name/Version
- GetPurchaseEvaluationByIdQuery handler load workflow info join
- Update Phase guard: cho sửa cả DangSoanThao + TraLai (Trả lại =
editable per Session 17 spec)
FE (cả 2 app mirror):
- types/purchaseEvaluation.ts: PeDetail +approvalWorkflowId/Code/Name/Version
- PeWorkspaceCreateView.tsx:
- Replace field disabled "Loại quy trình" → Select bắt buộc
- useQuery `/api/approval-workflows-v2?applicableType=N` filter theo
defaultType (1=DuyetNcc / 2=DuyetNccPhuongAn)
- Display option: "QT-DN-V2-001 v01 — Quy trình Duyệt NCC (đang áp dụng)"
- List cả version active + archived (UAT cần test compare)
- Empty state hint amber "Chưa có quy trình, vào /system/approval-workflows-v2"
- canSubmit require approvalWorkflowId set
- POST payload include approvalWorkflowId
Verify: dotnet build OK · 81 test pass · npm build × 2 OK · Mig 23 applied
cả 2 LocalDB.
Logic Service PE chưa wire qua ApprovalWorkflowId — vẫn pin
WorkflowDefinitionId Mig 21 legacy chạy. Session sau wire Service iterate
ApprovalWorkflowSteps + match approver theo schema V2 + drop legacy.
This commit is contained in:
@ -129,6 +129,12 @@ public record PurchaseEvaluationDetailBundleDto(
|
||||
BudgetSummaryDto? Budget,
|
||||
string? BudgetManualName,
|
||||
decimal? BudgetManualAmount,
|
||||
// Mig 23 — schema mới ApprovalWorkflowsV2 pin lúc create. Hiển thị Code +
|
||||
// Name + Version để FE show "QT-DN-V2-001 - Quy trình Duyệt NCC (v01)".
|
||||
Guid? ApprovalWorkflowId,
|
||||
string? ApprovalWorkflowCode,
|
||||
string? ApprovalWorkflowName,
|
||||
int? ApprovalWorkflowVersion,
|
||||
List<PurchaseEvaluationSupplierDto> Suppliers,
|
||||
List<PurchaseEvaluationDetailDto> Details,
|
||||
List<PurchaseEvaluationApprovalDto> Approvals,
|
||||
|
||||
@ -25,7 +25,8 @@ public record CreatePurchaseEvaluationCommand(
|
||||
string? PaymentTerms,
|
||||
Guid? BudgetId,
|
||||
string? BudgetManualName,
|
||||
decimal? BudgetManualAmount) : IRequest<Guid>;
|
||||
decimal? BudgetManualAmount,
|
||||
Guid? ApprovalWorkflowId = null) : IRequest<Guid>; // [Mig 23] User chọn quy trình duyệt V2 lúc tạo
|
||||
|
||||
public class CreatePurchaseEvaluationCommandValidator : AbstractValidator<CreatePurchaseEvaluationCommand>
|
||||
{
|
||||
@ -57,6 +58,23 @@ public class CreatePurchaseEvaluationCommandHandler(
|
||||
.Select(w => (Guid?)w.Id)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
// Validate ApprovalWorkflowId V2 (Mig 23) — User chọn lúc create.
|
||||
// Phải tồn tại + ApplicableType khớp với PE.Type.
|
||||
if (request.ApprovalWorkflowId is Guid awId)
|
||||
{
|
||||
var aw = await db.ApprovalWorkflows.AsNoTracking()
|
||||
.FirstOrDefaultAsync(w => w.Id == awId, ct)
|
||||
?? throw new NotFoundException("ApprovalWorkflow", awId);
|
||||
var expectedType = request.Type switch
|
||||
{
|
||||
PurchaseEvaluationType.DuyetNcc => Domain.ApprovalWorkflowsV2.ApprovalWorkflowApplicableType.DuyetNcc,
|
||||
PurchaseEvaluationType.DuyetNccPhuongAn => Domain.ApprovalWorkflowsV2.ApprovalWorkflowApplicableType.DuyetNccPhuongAn,
|
||||
_ => throw new ConflictException($"PurchaseEvaluationType {request.Type} chưa map sang ApprovalWorkflowApplicableType."),
|
||||
};
|
||||
if (aw.ApplicableType != expectedType)
|
||||
throw new ConflictException($"Quy trình {aw.Code} áp dụng cho {aw.ApplicableType}, không khớp với loại phiếu {request.Type}.");
|
||||
}
|
||||
|
||||
// Validate Budget link (nếu có): cùng Project + Phase=DaDuyet (chỉ cho
|
||||
// pick ngân sách đã duyệt mới được dùng làm reference đối chiếu).
|
||||
if (request.BudgetId is Guid bid)
|
||||
@ -81,6 +99,7 @@ public class CreatePurchaseEvaluationCommandHandler(
|
||||
MoTa = request.MoTa,
|
||||
DrafterUserId = currentUser.UserId,
|
||||
WorkflowDefinitionId = activeWfId,
|
||||
ApprovalWorkflowId = request.ApprovalWorkflowId, // Mig 23 — schema mới V2
|
||||
PaymentTerms = request.PaymentTerms,
|
||||
BudgetId = request.BudgetId,
|
||||
BudgetManualName = request.BudgetManualName,
|
||||
@ -120,7 +139,8 @@ public record UpdatePurchaseEvaluationDraftCommand(
|
||||
string? PaymentTerms,
|
||||
Guid? BudgetId,
|
||||
string? BudgetManualName,
|
||||
decimal? BudgetManualAmount) : IRequest;
|
||||
decimal? BudgetManualAmount,
|
||||
Guid? ApprovalWorkflowId = null) : IRequest; // [Mig 23] cho User đổi quy trình khi sửa Nháp
|
||||
|
||||
public class UpdatePurchaseEvaluationDraftCommandHandler(
|
||||
IApplicationDbContext db,
|
||||
@ -131,8 +151,25 @@ public class UpdatePurchaseEvaluationDraftCommandHandler(
|
||||
var entity = await db.PurchaseEvaluations.FirstOrDefaultAsync(x => x.Id == request.Id, ct)
|
||||
?? throw new NotFoundException("PurchaseEvaluation", request.Id);
|
||||
|
||||
if (entity.Phase != PurchaseEvaluationPhase.DangSoanThao)
|
||||
throw new ConflictException("Chỉ sửa được phiếu khi ở phase Đang soạn thảo.");
|
||||
if (entity.Phase != PurchaseEvaluationPhase.DangSoanThao
|
||||
&& entity.Phase != PurchaseEvaluationPhase.TraLai)
|
||||
throw new ConflictException("Chỉ sửa được phiếu khi ở phase Nháp hoặc Trả lại.");
|
||||
|
||||
// Validate ApprovalWorkflowId V2 nếu thay đổi (Mig 23).
|
||||
if (request.ApprovalWorkflowId is Guid awId && awId != entity.ApprovalWorkflowId)
|
||||
{
|
||||
var aw = await db.ApprovalWorkflows.AsNoTracking()
|
||||
.FirstOrDefaultAsync(w => w.Id == awId, ct)
|
||||
?? throw new NotFoundException("ApprovalWorkflow", awId);
|
||||
var expectedType = entity.Type switch
|
||||
{
|
||||
PurchaseEvaluationType.DuyetNcc => Domain.ApprovalWorkflowsV2.ApprovalWorkflowApplicableType.DuyetNcc,
|
||||
PurchaseEvaluationType.DuyetNccPhuongAn => Domain.ApprovalWorkflowsV2.ApprovalWorkflowApplicableType.DuyetNccPhuongAn,
|
||||
_ => throw new ConflictException($"PurchaseEvaluationType {entity.Type} chưa map sang ApprovalWorkflowApplicableType."),
|
||||
};
|
||||
if (aw.ApplicableType != expectedType)
|
||||
throw new ConflictException($"Quy trình {aw.Code} áp dụng cho {aw.ApplicableType}, không khớp với loại phiếu {entity.Type}.");
|
||||
}
|
||||
|
||||
// Validate Budget link nếu thay đổi.
|
||||
if (request.BudgetId is Guid bid && bid != entity.BudgetId)
|
||||
@ -153,6 +190,7 @@ public class UpdatePurchaseEvaluationDraftCommandHandler(
|
||||
entity.BudgetId = request.BudgetId;
|
||||
entity.BudgetManualName = request.BudgetManualName;
|
||||
entity.BudgetManualAmount = request.BudgetManualAmount;
|
||||
entity.ApprovalWorkflowId = request.ApprovalWorkflowId; // Mig 23 — User đổi quy trình
|
||||
|
||||
db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog
|
||||
{
|
||||
@ -409,6 +447,24 @@ public class GetPurchaseEvaluationQueryHandler(
|
||||
policy = PurchaseEvaluationPolicyRegistry.ForEvaluation(e);
|
||||
}
|
||||
|
||||
// Mig 23 — load ApprovalWorkflow V2 info nếu pin (Code/Name/Version
|
||||
// hiển thị FE detail card "QT-DN-V2-001 - Tên (v01)").
|
||||
string? awCode = null, awName = null;
|
||||
int? awVersion = null;
|
||||
if (e.ApprovalWorkflowId is Guid awId)
|
||||
{
|
||||
var aw = await db.ApprovalWorkflows.AsNoTracking()
|
||||
.Where(w => w.Id == awId)
|
||||
.Select(w => new { w.Code, w.Name, w.Version })
|
||||
.FirstOrDefaultAsync(ct);
|
||||
if (aw is not null)
|
||||
{
|
||||
awCode = aw.Code;
|
||||
awName = aw.Name;
|
||||
awVersion = aw.Version;
|
||||
}
|
||||
}
|
||||
|
||||
return new PurchaseEvaluationDetailBundleDto(
|
||||
e.Id, e.MaPhieu, e.Type, e.Phase, e.TenGoiThau, e.DiaDiem, e.MoTa,
|
||||
e.ProjectId, project?.Name ?? "",
|
||||
@ -419,6 +475,7 @@ public class GetPurchaseEvaluationQueryHandler(
|
||||
e.PaymentTerms, e.SlaDeadline, e.CreatedAt, e.UpdatedAt,
|
||||
e.BudgetId, budgetSummary,
|
||||
e.BudgetManualName, e.BudgetManualAmount,
|
||||
e.ApprovalWorkflowId, awCode, awName, awVersion,
|
||||
e.Suppliers
|
||||
.OrderBy(s => s.Order)
|
||||
.Select(s => new PurchaseEvaluationSupplierDto(
|
||||
|
||||
@ -18,7 +18,8 @@ public class PurchaseEvaluation : AuditableEntity
|
||||
public string? DiaDiem { get; set; } // Lô K, KCN Lộc An...
|
||||
public string? MoTa { get; set; }
|
||||
|
||||
public Guid? WorkflowDefinitionId { get; set; } // Pinned at create — config y như HĐ
|
||||
public Guid? WorkflowDefinitionId { get; set; } // [LEGACY Mig 21] Pinned at create — config y như HĐ
|
||||
public Guid? ApprovalWorkflowId { get; set; } // [Mig 23 Session 17] Pin schema mới ApprovalWorkflowsV2
|
||||
public DateTime? SlaDeadline { get; set; }
|
||||
public bool SlaWarningSent { get; set; }
|
||||
|
||||
|
||||
@ -27,9 +27,18 @@ public class PurchaseEvaluationConfiguration : IEntityTypeConfiguration<Purchase
|
||||
b.HasIndex(x => x.ProjectId);
|
||||
b.HasIndex(x => x.SlaDeadline);
|
||||
b.HasIndex(x => x.WorkflowDefinitionId);
|
||||
b.HasIndex(x => x.ApprovalWorkflowId);
|
||||
b.HasIndex(x => x.ContractId);
|
||||
b.HasIndex(x => x.BudgetId);
|
||||
|
||||
// FK ApprovalWorkflowId Restrict (Mig 23 Session 17) — schema mới
|
||||
// ApprovalWorkflowsV2 pin lúc create. Restrict để KHÔNG xóa workflow
|
||||
// nếu còn phiếu pin.
|
||||
b.HasOne<SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow>()
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.ApprovalWorkflowId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.HasMany(x => x.Suppliers).WithOne(s => s.PurchaseEvaluation).HasForeignKey(s => s.PurchaseEvaluationId).OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasMany(x => x.Details).WithOne(d => d.PurchaseEvaluation).HasForeignKey(d => d.PurchaseEvaluationId).OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasMany(x => x.Approvals).WithOne(a => a.PurchaseEvaluation).HasForeignKey(a => a.PurchaseEvaluationId).OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddApprovalWorkflowIdToPurchaseEvaluation : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "ApprovalWorkflowId",
|
||||
table: "PurchaseEvaluations",
|
||||
type: "uniqueidentifier",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PurchaseEvaluations_ApprovalWorkflowId",
|
||||
table: "PurchaseEvaluations",
|
||||
column: "ApprovalWorkflowId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_PurchaseEvaluations_ApprovalWorkflows_ApprovalWorkflowId",
|
||||
table: "PurchaseEvaluations",
|
||||
column: "ApprovalWorkflowId",
|
||||
principalTable: "ApprovalWorkflows",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_PurchaseEvaluations_ApprovalWorkflows_ApprovalWorkflowId",
|
||||
table: "PurchaseEvaluations");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_PurchaseEvaluations_ApprovalWorkflowId",
|
||||
table: "PurchaseEvaluations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ApprovalWorkflowId",
|
||||
table: "PurchaseEvaluations");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2485,6 +2485,9 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("ApprovalWorkflowId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("BudgetId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
@ -2578,6 +2581,8 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApprovalWorkflowId");
|
||||
|
||||
b.HasIndex("BudgetId");
|
||||
|
||||
b.HasIndex("ContractId");
|
||||
@ -3562,6 +3567,14 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", b =>
|
||||
{
|
||||
b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ApprovalWorkflowId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationApproval", b =>
|
||||
{
|
||||
b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation")
|
||||
|
||||
Reference in New Issue
Block a user