[CLAUDE] Workflow: wire ApproveV2 + LevelOpinions cho 4 WorkflowApps module (Phase 11 P11-A)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m6s

Wire full approval workflow V2 cho Leave/OT/Travel/Vehicle — cookie-cutter
mirror Proposal (Mig 38). Trước đây skeleton Phase 1 (Create+List), giờ
ApproveV2 advance-level + UPSERT LevelOpinion + atomic codegen.

Schema (Mig 41 WireWorkflowAppsApprovalV2 — 84→89 tables, pure additive):
- 4 bảng {Leave,Ot,Travel,Vehicle}LevelOpinions (UNIQUE composite + Cascade
  parent + Restrict Level — mirror ProposalLevelOpinion)
- 1 bảng WorkflowAppCodeSequences (shared atomic MaDonTu, Prefix-keyed)
- 4 cột RejectedFromStatus (smart return tracking)
- enum ApprovalWorkflowApplicableType.TravelRequest = 9

Application (LeaveOt + TravelVehicle ApprovalFeatures.cs — 30 handler):
- GetById detail (Include LevelOpinions + JOIN Step/Level) · UpdateDraft
- Submit (gen MaDonTu + DaGuiDuyet + level=1, verify ApplicableType per module)
- Approve (verify actor==ApproverUserId OR Admin, UPSERT opinion latest-write-wins,
  advance level OR terminal DaDuyet, empty comment → placeholder)
- Reject (→TuChoi) · Return (→TraLai + RejectedFromStatus)

Api: 4 controller +6 route mỗi cái (GET/{id}, PUT/{id}, submit/approve/reject/return)
Infra: DbInitializer seed 4 workflow V2 mẫu (QT-NP/OT/CT/XE-V2-001) → UAT test ngay
FE: WorkflowAppDetailPage.tsx declarative 4-kind (fe-admin+fe-user SHA256 identical)
  — workflow status + opinion timeline + action buttons; gỡ banner skeleton + row nav

Tests: +11 WorkflowAppApproveV2Tests (130→141 PASS) — state machine + UPSERT
  invariant + guards + codegen + forbidden + placeholder (Leave full + Ot smoke)

Verify: build 0 error · 141 test PASS · FE build ×2 · reviewer checklist
  (ApplicableType per-module + cross-module DbSet + [Authorize] — no copy-paste bug)
Known-minor (unreachable): Reject/Return actor-check skip nếu CurrentApprovalLevelOrder
  null — nhưng DaGuiDuyet luôn có set (defer hardening).
ItTicket KHÔNG đụng (kanban, no workflow V2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-30 09:44:00 +07:00
parent ad1dea9349
commit e7b66cd52b
39 changed files with 10604 additions and 22 deletions

View File

@ -55,6 +55,7 @@ public enum ApprovalWorkflowApplicableType
OtRequest = 6, // G-O4 — Đơn OT
VehicleBooking = 7, // G-O5 — Đặt xe công
ItTicket = 8, // G-O6 — Ticket CNTT
TravelRequest = 9, // G-O4 — Đơn công tác (Travel) — Phase 11 P11-A
}
// Bước = Phòng. 1 quy trình có nhiều bước theo Order.

View File

@ -19,4 +19,7 @@ public class LeaveRequest : AuditableEntity
public WorkflowAppStatus Status { get; set; } = WorkflowAppStatus.Nhap;
public Guid? ApprovalWorkflowId { get; set; } // pin ApplicableType=5
public int? CurrentApprovalLevelOrder { get; set; }
public WorkflowAppStatus? RejectedFromStatus { get; set; } // smart return tracking (mirror Proposal)
public List<LeaveRequestLevelOpinion> LevelOpinions { get; set; } = new();
}

View File

@ -0,0 +1,28 @@
using SolutionErp.Domain.ApprovalWorkflowsV2;
using SolutionErp.Domain.Common;
namespace SolutionErp.Domain.Office;
// Phase 11 P11-A (Mig 41) — Ý kiến cấp duyệt V2 dynamic cho LeaveRequest.
// Cookie-cutter mirror ProposalLevelOpinion (Mig 38).
//
// Mỗi row = 1 (LeaveRequest × ApprovalWorkflowLevel). Service ApproveV2Async sau
// khi approve thành công Cấp hiện tại sẽ UPSERT row này (latest-write-wins).
// Reject (TraLai/TuChoi) KHÔNG sync.
//
// UNIQUE composite (LeaveRequestId, ApprovalWorkflowLevelId) — 1 row / level / đơn.
// FK Cascade LeaveRequest (wipe khi xoá) + Restrict Level (admin xoá Level chặn).
// SignedByUserId track actor thật (có thể Admin override) + denorm FullName.
public class LeaveRequestLevelOpinion : AuditableEntity
{
public Guid LeaveRequestId { get; set; }
public Guid ApprovalWorkflowLevelId { get; set; }
public string? Comment { get; set; } // max 2000 hoặc placeholder
public DateTime SignedAt { get; set; }
public Guid SignedByUserId { get; set; } // người ký thực sự (có thể Admin thay)
public string SignedByFullName { get; set; } = string.Empty; // snapshot denorm
public LeaveRequest? LeaveRequest { get; set; }
public ApprovalWorkflowLevel? Level { get; set; }
}

View File

@ -19,4 +19,7 @@ public class OtRequest : AuditableEntity
public WorkflowAppStatus Status { get; set; } = WorkflowAppStatus.Nhap;
public Guid? ApprovalWorkflowId { get; set; }
public int? CurrentApprovalLevelOrder { get; set; }
public WorkflowAppStatus? RejectedFromStatus { get; set; } // smart return tracking (mirror Proposal)
public List<OtRequestLevelOpinion> LevelOpinions { get; set; } = new();
}

View File

@ -0,0 +1,28 @@
using SolutionErp.Domain.ApprovalWorkflowsV2;
using SolutionErp.Domain.Common;
namespace SolutionErp.Domain.Office;
// Phase 11 P11-A (Mig 41) — Ý kiến cấp duyệt V2 dynamic cho OtRequest.
// Cookie-cutter mirror ProposalLevelOpinion (Mig 38).
//
// Mỗi row = 1 (OtRequest × ApprovalWorkflowLevel). Service ApproveV2Async sau
// khi approve thành công Cấp hiện tại sẽ UPSERT row này (latest-write-wins).
// Reject (TraLai/TuChoi) KHÔNG sync.
//
// UNIQUE composite (OtRequestId, ApprovalWorkflowLevelId) — 1 row / level / đơn.
// FK Cascade OtRequest (wipe khi xoá) + Restrict Level (admin xoá Level chặn).
// SignedByUserId track actor thật (có thể Admin override) + denorm FullName.
public class OtRequestLevelOpinion : AuditableEntity
{
public Guid OtRequestId { get; set; }
public Guid ApprovalWorkflowLevelId { get; set; }
public string? Comment { get; set; } // max 2000 hoặc placeholder
public DateTime SignedAt { get; set; }
public Guid SignedByUserId { get; set; } // người ký thực sự (có thể Admin thay)
public string SignedByFullName { get; set; } = string.Empty; // snapshot denorm
public OtRequest? OtRequest { get; set; }
public ApprovalWorkflowLevel? Level { get; set; }
}

View File

@ -18,4 +18,7 @@ public class TravelRequest : AuditableEntity
public WorkflowAppStatus Status { get; set; } = WorkflowAppStatus.Nhap;
public Guid? ApprovalWorkflowId { get; set; }
public int? CurrentApprovalLevelOrder { get; set; }
public WorkflowAppStatus? RejectedFromStatus { get; set; } // smart return tracking (mirror Proposal)
public List<TravelRequestLevelOpinion> LevelOpinions { get; set; } = new();
}

View File

@ -0,0 +1,28 @@
using SolutionErp.Domain.ApprovalWorkflowsV2;
using SolutionErp.Domain.Common;
namespace SolutionErp.Domain.Office;
// Phase 11 P11-A (Mig 41) — Ý kiến cấp duyệt V2 dynamic cho TravelRequest.
// Cookie-cutter mirror ProposalLevelOpinion (Mig 38).
//
// Mỗi row = 1 (TravelRequest × ApprovalWorkflowLevel). Service ApproveV2Async sau
// khi approve thành công Cấp hiện tại sẽ UPSERT row này (latest-write-wins).
// Reject (TraLai/TuChoi) KHÔNG sync.
//
// UNIQUE composite (TravelRequestId, ApprovalWorkflowLevelId) — 1 row / level / đơn.
// FK Cascade TravelRequest (wipe khi xoá) + Restrict Level (admin xoá Level chặn).
// SignedByUserId track actor thật (có thể Admin override) + denorm FullName.
public class TravelRequestLevelOpinion : AuditableEntity
{
public Guid TravelRequestId { get; set; }
public Guid ApprovalWorkflowLevelId { get; set; }
public string? Comment { get; set; } // max 2000 hoặc placeholder
public DateTime SignedAt { get; set; }
public Guid SignedByUserId { get; set; } // người ký thực sự (có thể Admin thay)
public string SignedByFullName { get; set; } = string.Empty; // snapshot denorm
public TravelRequest? TravelRequest { get; set; }
public ApprovalWorkflowLevel? Level { get; set; }
}

View File

@ -20,4 +20,7 @@ public class VehicleBooking : AuditableEntity
public WorkflowAppStatus Status { get; set; } = WorkflowAppStatus.Nhap;
public Guid? ApprovalWorkflowId { get; set; }
public int? CurrentApprovalLevelOrder { get; set; }
public WorkflowAppStatus? RejectedFromStatus { get; set; } // smart return tracking (mirror Proposal)
public List<VehicleBookingLevelOpinion> LevelOpinions { get; set; } = new();
}

View File

@ -0,0 +1,28 @@
using SolutionErp.Domain.ApprovalWorkflowsV2;
using SolutionErp.Domain.Common;
namespace SolutionErp.Domain.Office;
// Phase 11 P11-A (Mig 41) — Ý kiến cấp duyệt V2 dynamic cho VehicleBooking.
// Cookie-cutter mirror ProposalLevelOpinion (Mig 38).
//
// Mỗi row = 1 (VehicleBooking × ApprovalWorkflowLevel). Service ApproveV2Async sau
// khi approve thành công Cấp hiện tại sẽ UPSERT row này (latest-write-wins).
// Reject (TraLai/TuChoi) KHÔNG sync.
//
// UNIQUE composite (VehicleBookingId, ApprovalWorkflowLevelId) — 1 row / level / đơn.
// FK Cascade VehicleBooking (wipe khi xoá) + Restrict Level (admin xoá Level chặn).
// SignedByUserId track actor thật (có thể Admin override) + denorm FullName.
public class VehicleBookingLevelOpinion : AuditableEntity
{
public Guid VehicleBookingId { get; set; }
public Guid ApprovalWorkflowLevelId { get; set; }
public string? Comment { get; set; } // max 2000 hoặc placeholder
public DateTime SignedAt { get; set; }
public Guid SignedByUserId { get; set; } // người ký thực sự (có thể Admin thay)
public string SignedByFullName { get; set; } = string.Empty; // snapshot denorm
public VehicleBooking? VehicleBooking { get; set; }
public ApprovalWorkflowLevel? Level { get; set; }
}

View File

@ -0,0 +1,19 @@
namespace SolutionErp.Domain.Office;
// Phase 11 P11-A (Mig 41) — Sequence generator dùng chung cho mã đơn từ (MaDonTu)
// của 4 WorkflowApps module (Leave / OT / Travel / VehicleBooking).
// Mirror ProposalCodeSequence pattern (Prefix string PK + LastSeq atomic).
//
// Prefix-keyed per module per năm, vd:
// "DT/LR/2026" (Leave) → "DT/LR/2026/001" → "DT/LR/2026/002" → ...
// "DT/OT/2026" (OT)
// "DT/CT/2026" (Travel — Công tác)
// "DX/XE/2026" (VehicleBooking — Đặt xe)
// LastSeq reset đầu năm tự nhiên (key Prefix mới). Update atomic qua
// SERIALIZABLE transaction trong CodeGen service.
public class WorkflowAppCodeSequence
{
public string Prefix { get; set; } = string.Empty; // PK — "DT/LR/2026"
public int LastSeq { get; set; }
public DateTime UpdatedAt { get; set; }
}