[CLAUDE] PurchaseEvaluation: Chunk B Service V2 hook UPSERT opinion + DTO + GET include
Service `ApproveV2Async` sau khi log approval (Decision=Approve) → UPSERT row `PurchaseEvaluationLevelOpinions` cho Cấp hiện tại (auto sync ý kiến từ comment khi duyệt). Reject KHÔNG sync. Match level theo ApproverUserId của actor (multi-NV cùng Cấp OR-of-N). Admin override (actor.Id KHÔNG match) → fallback first level — FE detect SignedByUserId !== Level.ApproverUserId hiển thị "Admin duyệt thay". Empty/whitespace comment → "(duyệt — không ý kiến)" placeholder (Q4 bonus). Helper `ResolveActorFullNameAsync(actorUserId, isSystem, ct)` lookup denorm SignedByFullName từ Users (fallback "(System)" / "(unknown)"). DTO `PurchaseEvaluationLevelOpinionDto` (15 fields): - StepOrder/StepName/StepDepartmentId/StepDepartmentName (Bước Phòng) - LevelOrder/LevelName/ApproverUserId/ApproverFullName (Cấp NV) - Comment/SignedAt/SignedByUserId/SignedByFullName (sign-off) GetPurchaseEvaluationQueryHandler: - Include LevelOpinions - helper BuildLevelOpinionsAsync JOIN ApprovalWorkflows.Steps.Levels + Departments + Users → denorm DTO. Empty list cho phiếu V1 / V2 chưa có cấp nào duyệt → FE fallback message. Verify: dotnet build pass + dotnet test 81 pass (no regression). Chunk C kế tiếp: FE Section 5 dynamic mirror 2 app.
This commit is contained in:
@ -142,6 +142,27 @@ public record PurchaseEvaluationDepartmentOpinionDto(
|
||||
Guid? UserId,
|
||||
string? UserName);
|
||||
|
||||
// Mig 26 (Session 19) — Ý kiến cấp duyệt V2 dynamic theo ApprovalWorkflowLevel.
|
||||
// FE Section 5 render dynamic: forEach Step → forEach Level → 1 OpinionBox.
|
||||
// Service ApproveV2Async UPSERT tự động khi NV duyệt (Q1=1B). Comment empty
|
||||
// fallback "(duyệt — không ý kiến)". `SignedByUserId !== ApproverUserId` →
|
||||
// FE show banner "Admin duyệt thay <ApproverFullName>".
|
||||
public record PurchaseEvaluationLevelOpinionDto(
|
||||
Guid Id,
|
||||
Guid ApprovalWorkflowLevelId,
|
||||
int StepOrder,
|
||||
string StepName,
|
||||
Guid? StepDepartmentId,
|
||||
string? StepDepartmentName,
|
||||
int LevelOrder,
|
||||
string? LevelName,
|
||||
Guid ApproverUserId,
|
||||
string? ApproverFullName,
|
||||
string Comment,
|
||||
DateTime SignedAt,
|
||||
Guid SignedByUserId,
|
||||
string SignedByFullName);
|
||||
|
||||
public record PurchaseEvaluationDetailBundleDto(
|
||||
Guid Id,
|
||||
string? MaPhieu,
|
||||
@ -180,4 +201,7 @@ public record PurchaseEvaluationDetailBundleDto(
|
||||
List<PurchaseEvaluationApprovalDto> Approvals,
|
||||
List<PurchaseEvaluationAttachmentDto> Attachments,
|
||||
List<PurchaseEvaluationDepartmentOpinionDto> DepartmentOpinions,
|
||||
// Mig 26 (Session 19) — Section 5 V2 dynamic. Empty list cho phiếu V1
|
||||
// legacy hoặc phiếu V2 chưa có cấp nào duyệt → FE fallback message.
|
||||
List<PurchaseEvaluationLevelOpinionDto> LevelOpinions,
|
||||
PurchaseEvaluationWorkflowSummaryDto Workflow);
|
||||
|
||||
@ -447,6 +447,7 @@ public class GetPurchaseEvaluationQueryHandler(
|
||||
.Include(x => x.Approvals)
|
||||
.Include(x => x.Attachments)
|
||||
.Include(x => x.DepartmentOpinions)
|
||||
.Include(x => x.LevelOpinions) // Mig 26 — Section 5 V2 dynamic
|
||||
.FirstOrDefaultAsync(x => x.Id == request.Id, ct)
|
||||
?? throw new NotFoundException("PurchaseEvaluation", request.Id);
|
||||
|
||||
@ -687,12 +688,73 @@ public class GetPurchaseEvaluationQueryHandler(
|
||||
o.Id, o.Kind, KindLabel(o.Kind),
|
||||
o.Opinion, o.SignedAt, o.UserId, o.UserName))
|
||||
.ToList(),
|
||||
await BuildLevelOpinionsAsync(e, ct),
|
||||
new PurchaseEvaluationWorkflowSummaryDto(
|
||||
policy.Name, policy.Description,
|
||||
policy.ActivePhases.ToList(),
|
||||
policy.NextPhasesFrom(e.Phase).ToList()));
|
||||
}
|
||||
|
||||
// Mig 26 (Session 19) — Build LevelOpinionDto[] cho Section 5 dynamic.
|
||||
// Phiếu V1 (no ApprovalWorkflowId) hoặc chưa có cấp duyệt nào → empty list,
|
||||
// FE hiển thị fallback message. JOIN Steps/Levels lấy meta (StepOrder, name,
|
||||
// DepartmentName, ApproverFullName) — denorm vào DTO để FE render trực tiếp.
|
||||
private async Task<List<PurchaseEvaluationLevelOpinionDto>> BuildLevelOpinionsAsync(
|
||||
PurchaseEvaluation e, CancellationToken ct)
|
||||
{
|
||||
var result = new List<PurchaseEvaluationLevelOpinionDto>();
|
||||
if (e.LevelOpinions.Count == 0 || e.ApprovalWorkflowId is not Guid wfV2Id)
|
||||
return result;
|
||||
|
||||
var aw = await db.ApprovalWorkflows.AsNoTracking()
|
||||
.Include(w => w.Steps).ThenInclude(s => s.Levels)
|
||||
.FirstOrDefaultAsync(w => w.Id == wfV2Id, ct);
|
||||
if (aw is null) return result;
|
||||
|
||||
var levelMap = aw.Steps
|
||||
.SelectMany(s => s.Levels.Select(l => new { Step = s, Level = l }))
|
||||
.ToDictionary(x => x.Level.Id);
|
||||
|
||||
var deptIds = aw.Steps.Where(s => s.DepartmentId.HasValue)
|
||||
.Select(s => s.DepartmentId!.Value).Distinct().ToList();
|
||||
var depts = await db.Departments.AsNoTracking()
|
||||
.Where(d => deptIds.Contains(d.Id))
|
||||
.ToDictionaryAsync(d => d.Id, d => d.Name, ct);
|
||||
|
||||
var userIds = new HashSet<Guid>(
|
||||
aw.Steps.SelectMany(s => s.Levels.Select(l => l.ApproverUserId)));
|
||||
// SignedByUserId có thể KHÁC ApproverUserId (Admin override) — load thêm.
|
||||
foreach (var op in e.LevelOpinions) userIds.Add(op.SignedByUserId);
|
||||
var users = await userManager.Users.AsNoTracking()
|
||||
.Where(u => userIds.Contains(u.Id))
|
||||
.ToDictionaryAsync(u => u.Id, u => u.FullName, ct);
|
||||
|
||||
foreach (var op in e.LevelOpinions.OrderBy(o => o.SignedAt))
|
||||
{
|
||||
if (!levelMap.TryGetValue(op.ApprovalWorkflowLevelId, out var meta)) continue;
|
||||
string? deptName = meta.Step.DepartmentId.HasValue
|
||||
&& depts.TryGetValue(meta.Step.DepartmentId.Value, out var dn)
|
||||
? dn : null;
|
||||
string? approverFullName = users.TryGetValue(meta.Level.ApproverUserId, out var an) ? an : null;
|
||||
result.Add(new PurchaseEvaluationLevelOpinionDto(
|
||||
op.Id,
|
||||
op.ApprovalWorkflowLevelId,
|
||||
meta.Step.Order,
|
||||
meta.Step.Name,
|
||||
meta.Step.DepartmentId,
|
||||
deptName,
|
||||
meta.Level.Order,
|
||||
meta.Level.Name,
|
||||
meta.Level.ApproverUserId,
|
||||
approverFullName,
|
||||
op.Comment ?? "",
|
||||
op.SignedAt,
|
||||
op.SignedByUserId,
|
||||
op.SignedByFullName));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string KindLabel(PeDepartmentKind k) => k switch
|
||||
{
|
||||
PeDepartmentKind.PheDuyet => "Phê duyệt",
|
||||
|
||||
Reference in New Issue
Block a user