[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

@ -36,11 +36,26 @@ public class ContractWorkflowService(
if (contract.Phase == targetPhase)
throw new ConflictException("HĐ đã ở phase đích.");
// Admin may override the default policy per ContractType via the
// /system/workflows page. Load all overrides once (7 rows max).
var overrides = await db.WorkflowTypeAssignments.AsNoTracking()
.ToDictionaryAsync(a => a.ContractType, a => a.PolicyName, ct);
var policy = WorkflowPolicyRegistry.ForContractWithOverrides(contract, overrides);
// Resolve the workflow: prefer the pinned WorkflowDefinition (new
// versioned system), else fall back to the static/override registry
// (legacy path for contracts created before versioning rolled out).
WorkflowPolicy policy;
if (contract.WorkflowDefinitionId is Guid wfId)
{
var def = await db.WorkflowDefinitions.AsNoTracking()
.Include(d => d.Steps.OrderBy(s => s.Order))
.ThenInclude(s => s.Approvers)
.FirstOrDefaultAsync(d => d.Id == wfId, ct);
policy = def is not null
? WorkflowPolicyRegistry.FromDefinition(def)
: WorkflowPolicyRegistry.ForContract(contract);
}
else
{
var overrides = await db.WorkflowTypeAssignments.AsNoTracking()
.ToDictionaryAsync(a => a.ContractType, a => a.PolicyName, ct);
policy = WorkflowPolicyRegistry.ForContractWithOverrides(contract, overrides);
}
var isAdmin = actorRoles.Contains(AppRoles.Admin);
var isSystem = actorUserId is null && decision == ApprovalDecision.AutoApprove;