[CLAUDE] Infra: Plan B Chunk B2 — UPSERT ContractLevelOpinion + ResolveActorFullName helper
Replace TODO marker trong Chunk B138469d(line 257-262) bằng UPSERT block mirror PE Mig 26 line 512-546. Changes: - ApproveV2Async: move matchingLevel computation UP (trước UPSERT block) - +UPSERT ContractLevelOpinion ~25 LOC: - Match level theo ApproverUserId (OR-of-N) + fallback first (admin override) - Empty comment → "(duyệt — không ý kiến)" placeholder - Insert mới hoặc update existing (UPSERT semantic) - SignedByUserId + SignedByFullName denormalized cho Section 5 FE - skipToFinal block reuse matchingLevel (KHÔNG re-compute) - +ResolveActorFullNameAsync helper (mirror PE line 774-783) Section 5 FE (Chunk E) sẽ render dynamic theo flow.steps[].levels[] với opinion data từ table này. Admin override → FE detect SignedByUserId !== Level.ApproverUserId → banner "Admin duyệt thay". Verify: - dotnet build SolutionErp.slnx PASS 0 err, 2 pre-existing DocxRenderer warn - dotnet test 111/111 PASS — 0 regression - V1 legacy path UNCHANGED (7 prod contract giữ behavior) Plan B chain status: - A158898e8✅ Entity +2 fields - A2a85e437✅ Mig 32 + Config + Seed - B138469d✅ Service ApproveV2Async branch (UPSERT TODO) - C26c98d3✅ Mig 33 ContractLevelOpinions - B2 (this) ✅ UPSERT block (resolve TODO Chunk B) - D FE Workspace V2 (Implementer, next) - E FE Section 5 V2 (Implementer, pending) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -279,18 +279,46 @@ public class ContractWorkflowService(
|
||||
ApprovedAt = dateTime.UtcNow,
|
||||
});
|
||||
|
||||
// TODO Plan B Chunk C: UPSERT ContractLevelOpinion vào row Level chính
|
||||
// chủ (mirror PE Mig 26 / line 512-546). Bảng + entity chưa có (Chunk C
|
||||
// Implementer sẽ scaffold Mig 33). Block UPSERT add ở đây sau Chunk C
|
||||
// done. Hiện tại Section 5 FE sẽ KHÔNG hiển thị opinion comment per
|
||||
// Level — sẽ wire khi Chunk E (FE Section 5 LevelOpinionsV2).
|
||||
// [Plan B Chunk B2 S29 2026-05-22] UPSERT ContractLevelOpinion vào row
|
||||
// Level chính chủ (mirror PE Mig 26 line 512-546). Section 5 FE render
|
||||
// dynamic theo flow.steps[].levels[]. Comment khi duyệt auto sync sang
|
||||
// Section 5 (read-only summary). Empty comment → "(duyệt — không ý kiến)"
|
||||
// placeholder. 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.ContractLevelOpinions
|
||||
.FirstOrDefaultAsync(o => o.ContractId == contract.Id
|
||||
&& o.ApprovalWorkflowLevelId == matchingLevel.Id, ct);
|
||||
var normalizedComment = string.IsNullOrWhiteSpace(comment)
|
||||
? "(duyệt — không ý kiến)"
|
||||
: comment.Trim();
|
||||
if (existingOpinion is null)
|
||||
{
|
||||
db.ContractLevelOpinions.Add(new ContractLevelOpinion
|
||||
{
|
||||
ContractId = contract.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;
|
||||
}
|
||||
|
||||
// skipToFinal F2 (Mig 31 Plan K S23) — Approver scope ChoDuyet skip
|
||||
// thẳng Cấp cuối. Admin opt-in per slot tại AllowApproverSkipToFinal.
|
||||
if (skipToFinal)
|
||||
{
|
||||
var matchingLevel = pendingLevelGroup.FirstOrDefault(l => actorUserId.HasValue && l.ApproverUserId == actorUserId.Value)
|
||||
?? pendingLevelGroup.First();
|
||||
if (!isAdmin && !isSystem && !matchingLevel.AllowApproverSkipToFinal)
|
||||
{
|
||||
throw new ConflictException(
|
||||
@ -398,4 +426,18 @@ public class ContractWorkflowService(
|
||||
ct: ct);
|
||||
}
|
||||
}
|
||||
|
||||
// [Plan B Chunk B2 S29 2026-05-22] Resolve actor full name for
|
||||
// ContractLevelOpinion.SignedByFullName denormalized field (Section 5 FE
|
||||
// display). Mirror PE PurchaseEvaluationWorkflowService.cs:774-783.
|
||||
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