[CLAUDE] App: Plan B Chunk E2 — ContractDetailDto +ApprovalWorkflowId + LevelOpinions[] populate
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<ContractLevelOpinionDto>? 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) <noreply@anthropic.com>
This commit is contained in:
@ -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<ContractLevelOpinionDto>? 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<ContractLevelOpinionDto>();
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve user names
|
||||
var userIds = new HashSet<Guid>();
|
||||
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
|
||||
|
||||
@ -46,7 +46,34 @@ public record ContractDetailDto(
|
||||
List<ContractApprovalDto> Approvals,
|
||||
List<ContractCommentDto> Comments,
|
||||
List<ContractAttachmentDto> 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<ContractLevelOpinionDto>? 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).
|
||||
|
||||
Reference in New Issue
Block a user