[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:
@ -187,6 +187,42 @@ public class PurchaseEvaluationWorkflowService(
|
||||
ApprovedAt = dateTime.UtcNow,
|
||||
});
|
||||
|
||||
// Mig 26 (Session 19) — UPSERT opinion vào row Level chính chủ. Section 5
|
||||
// FE render dynamic theo flow.steps[].levels[]. Q1=1B chốt: comment khi
|
||||
// duyệt auto sync sang Section 5 (read-only summary). Empty comment →
|
||||
// "(duyệt — không ý kiến)" placeholder Q4 bonus.
|
||||
// Multi-NV cùng Cấp (OR-of-N): match level theo ApproverUserId. Admin
|
||||
// override → fallback first level group; FE detect SignedByUserId !==
|
||||
// Level.ApproverUserId → banner "Admin duyệt thay".
|
||||
var matchingLevel = pendingLevelGroup.FirstOrDefault(l => actorUserId.HasValue && l.ApproverUserId == actorUserId.Value)
|
||||
?? pendingLevelGroup.First();
|
||||
var actorFullName = await ResolveActorFullNameAsync(actorUserId, isSystem, ct);
|
||||
var existingOpinion = await db.PurchaseEvaluationLevelOpinions
|
||||
.FirstOrDefaultAsync(o => o.PurchaseEvaluationId == evaluation.Id
|
||||
&& o.ApprovalWorkflowLevelId == matchingLevel.Id, ct);
|
||||
var normalizedComment = string.IsNullOrWhiteSpace(comment)
|
||||
? "(duyệt — không ý kiến)"
|
||||
: comment.Trim();
|
||||
if (existingOpinion is null)
|
||||
{
|
||||
db.PurchaseEvaluationLevelOpinions.Add(new PurchaseEvaluationLevelOpinion
|
||||
{
|
||||
PurchaseEvaluationId = evaluation.Id,
|
||||
ApprovalWorkflowLevelId = matchingLevel.Id,
|
||||
Comment = normalizedComment,
|
||||
SignedAt = dateTime.UtcNow,
|
||||
SignedByUserId = actorUserId ?? Guid.Empty,
|
||||
SignedByFullName = actorFullName,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
existingOpinion.Comment = normalizedComment;
|
||||
existingOpinion.SignedAt = dateTime.UtcNow;
|
||||
existingOpinion.SignedByUserId = actorUserId ?? Guid.Empty;
|
||||
existingOpinion.SignedByFullName = actorFullName;
|
||||
}
|
||||
|
||||
// Advance: nếu còn cấp tiếp trong Step → levelOrder++; else → next Step + level 1
|
||||
if (currentLevelOrder < maxLevelOrder)
|
||||
{
|
||||
@ -353,4 +389,18 @@ public class PurchaseEvaluationWorkflowService(
|
||||
ct: ct);
|
||||
}
|
||||
}
|
||||
|
||||
// Mig 26 (Session 19) — helper resolve FullName cho denorm `SignedByFullName`.
|
||||
// System auto-approve (actorUserId null + isSystem) → "(System)". User không
|
||||
// tồn tại / xóa → fallback UserName / "(unknown)".
|
||||
private async Task<string> ResolveActorFullNameAsync(Guid? actorUserId, bool isSystem, CancellationToken ct)
|
||||
{
|
||||
if (isSystem || actorUserId is null) return "(System)";
|
||||
var user = await db.Users.AsNoTracking()
|
||||
.Where(u => u.Id == actorUserId.Value)
|
||||
.Select(u => new { u.FullName, u.UserName })
|
||||
.FirstOrDefaultAsync(ct);
|
||||
if (user is null) return "(unknown)";
|
||||
return !string.IsNullOrWhiteSpace(user.FullName) ? user.FullName : (user.UserName ?? "(unknown)");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user