From 48f6d22b3d07b6cb21d6949e6f9692abe7d3f288 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Fri, 22 May 2026 12:38:38 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20App:=20Plan=20B=20Chunk=20E2=20?= =?UTF-8?q?=E2=80=94=20ContractDetailDto=20+ApprovalWorkflowId=20+=20Level?= =?UTF-8?q?Opinions[]=20populate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror PE PeDetailBundle pattern. Expose V2 workflow state cho FE Section 5 Chunk E3 (Implementer pending) render dynamic LevelOpinionsSectionV2. Changes: - ContractDtos.cs: - ContractDetailDto +3 fields (default null backward compat): - Guid? ApprovalWorkflowId (V2 pin) - int? CurrentApprovalLevelOrder - List? LevelOpinions - NEW record ContractLevelOpinionDto (mirror PE 12 fields) - ContractFeatures.cs GetContractQueryHandler: - Load LevelOpinions via 3-step JOIN (ContractLevelOpinions + ApprovalWorkflowLevels.Include(Step) + Users) - Map to ContractLevelOpinionDto với StepOrder/Name + LevelOrder/Name + Approver/SignedBy resolve - OrderBy StepOrder + LevelOrder - Null fallback Comment "" (CS8604 silence) - Empty list khi V2 pin nhưng KHÔNG có opinion (workflow start lúc Drafter trình) - Skip load nếu V1 (ApprovalWorkflowId null) → null marker FE detect FE Chunk E3 sẽ: - Detect V2 mode qua `bundle.approvalWorkflowId != null` - Fetch ApprovalFlow shape via existing /api/approval-workflows-v2/{ApprovalWorkflowId} - Render Section 5 dynamic forEach Step → forEach Level → 1 OpinionBox với opinion data from LevelOpinions[] Verify: - dotnet build PASS 0 err, 0 warn (clean) - dotnet test 111/111 PASS — 0 regression - V1 legacy contract Detail unchanged (ApprovalWorkflowId=null + LevelOpinions=null) Plan B chain status (8/9 chunks done): - A1 58898e8 ✅ Entity - A2 a85e437 ✅ Mig 32 + Seed - B 138469d ✅ Service ApproveV2 branch - C 26c98d3 ✅ Mig 33 LevelOpinions - B2 1f199b0 ✅ UPSERT block - E1 ef23308 ✅ CreateContractCommand +V2 - D 62b50d1 ✅ FE Workspace V2 - E2 (this) ✅ ContractDetailDto +V2 + LevelOpinions populate - E3 FE Section 5 LevelOpinionsV2 (Implementer next) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Contracts/ContractFeatures.cs | 54 ++++++++++++++++++- .../Contracts/Dtos/ContractDtos.cs | 29 +++++++++- 2 files changed, 81 insertions(+), 2 deletions(-) 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).