[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

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:
pqhuy1987
2026-05-08 14:34:54 +07:00
parent d642fd361e
commit 0a40c65421
11 changed files with 4044 additions and 23 deletions

View File

@ -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,

View File

@ -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(