diff --git a/src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs b/src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs index 4088ac7..933c8ff 100644 --- a/src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs +++ b/src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs @@ -477,6 +477,54 @@ public class GetContractQueryHandler( workflowPolicy = WorkflowPolicyRegistry.ForContractWithOverrides(c, workflowOverrides); } + // [Plan B S29 2026-05-22 Chunk E2] Load V2 LevelOpinions nếu pin ApprovalWorkflowId. + // JOIN ApprovalWorkflowLevel + Step + Approver User để build dynamic DTO + // cho FE Section 5 render dynamic theo flow shape. V1 legacy → empty list. + List? levelOpinionsDto = null; + if (c.ApprovalWorkflowId is Guid awIdLoad) + { + var opinions = await db.ContractLevelOpinions.AsNoTracking() + .Where(o => o.ContractId == c.Id) + .ToListAsync(ct); + if (opinions.Count > 0) + { + var levelIds = opinions.Select(o => o.ApprovalWorkflowLevelId).ToHashSet(); + var levels = await db.ApprovalWorkflowLevels.AsNoTracking() + .Include(l => l.Step) + .Where(l => levelIds.Contains(l.Id)) + .ToListAsync(ct); + var approverUserIds = levels.Select(l => l.ApproverUserId).ToHashSet(); + var approverNames = await userManager.Users.AsNoTracking() + .Where(u => approverUserIds.Contains(u.Id)) + .ToDictionaryAsync(u => u.Id, u => u.FullName, ct); + levelOpinionsDto = opinions + .Select(o => + { + var level = levels.FirstOrDefault(l => l.Id == o.ApprovalWorkflowLevelId); + var step = level?.Step; + return new ContractLevelOpinionDto( + o.Id, + o.ApprovalWorkflowLevelId, + StepOrder: step?.Order ?? 0, + StepName: step?.Name ?? "", + LevelOrder: level?.Order ?? 0, + LevelName: level?.Name, + ApproverUserId: level?.ApproverUserId ?? Guid.Empty, + ApproverFullName: level != null && approverNames.TryGetValue(level.ApproverUserId, out var afn) ? afn : null, + o.Comment ?? "", + o.SignedAt, + o.SignedByUserId, + o.SignedByFullName); + }) + .OrderBy(d => d.StepOrder).ThenBy(d => d.LevelOrder) + .ToList(); + } + else + { + levelOpinionsDto = new List(); + } + } + // Resolve user names var userIds = new HashSet(); if (c.DrafterUserId is Guid did) userIds.Add(did); @@ -516,7 +564,11 @@ public class GetContractQueryHandler( att.Id, att.FileName, att.StoragePath, att.FileSize, att.ContentType, att.Purpose, att.Note, att.CreatedAt)) .ToList(), - BuildWorkflowSummary(c, workflowPolicy)); + BuildWorkflowSummary(c, workflowPolicy), + // [Plan B S29 2026-05-22 Chunk E2] V2 fields + ApprovalWorkflowId: c.ApprovalWorkflowId, + CurrentApprovalLevelOrder: c.CurrentApprovalLevelOrder, + LevelOpinions: levelOpinionsDto); } // FE uses this to render next-phase buttons dynamically — no more hardcoded diff --git a/src/Backend/SolutionErp.Application/Contracts/Dtos/ContractDtos.cs b/src/Backend/SolutionErp.Application/Contracts/Dtos/ContractDtos.cs index 1a36413..2591a44 100644 --- a/src/Backend/SolutionErp.Application/Contracts/Dtos/ContractDtos.cs +++ b/src/Backend/SolutionErp.Application/Contracts/Dtos/ContractDtos.cs @@ -46,7 +46,34 @@ public record ContractDetailDto( List Approvals, List Comments, List Attachments, - WorkflowSummaryDto Workflow); + WorkflowSummaryDto Workflow, + // [Plan B S29 2026-05-22 Chunk E2] V2 workflow fields — mirror PE pattern. + // ApprovalWorkflowId pin lúc create (Mig 32). FE Section 5 detect V2 mode + // qua field này: nếu Guid → render dynamic LevelOpinionsSectionV2; + // nếu null → V1 legacy KHÔNG Section 5 V2. + // FE fetch ApprovalFlow shape via /api/approval-workflows-v2/{ApprovalWorkflowId}. + Guid? ApprovalWorkflowId = null, + int? CurrentApprovalLevelOrder = null, + List? LevelOpinions = null); + +// [Plan B S29 2026-05-22 Chunk E2] Ý kiến cấp duyệt V2 dynamic theo +// ApprovalWorkflowLevel. Mirror PE PurchaseEvaluationLevelOpinionDto pattern. +// Service ApproveV2Async UPSERT (Plan B Chunk B2 1f199b0). Comment empty +// fallback "(duyệt — không ý kiến)". SignedByUserId !== Level.ApproverUserId +// → FE show banner "Admin duyệt thay". +public record ContractLevelOpinionDto( + Guid Id, + Guid ApprovalWorkflowLevelId, + int StepOrder, + string StepName, + int LevelOrder, + string? LevelName, + Guid ApproverUserId, + string? ApproverFullName, + string Comment, + DateTime SignedAt, + Guid SignedByUserId, + string? SignedByFullName); // Policy snapshot for the FE — lets UI render next-phase buttons dynamically // without hardcoding the transition map (single source of truth in BE).