[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,
|
ApprovedAt = dateTime.UtcNow,
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO Plan B Chunk C: UPSERT ContractLevelOpinion vào row Level chính
|
// [Plan B Chunk B2 S29 2026-05-22] UPSERT ContractLevelOpinion vào row
|
||||||
// chủ (mirror PE Mig 26 / line 512-546). Bảng + entity chưa có (Chunk C
|
// Level chính chủ (mirror PE Mig 26 line 512-546). Section 5 FE render
|
||||||
// Implementer sẽ scaffold Mig 33). Block UPSERT add ở đây sau Chunk C
|
// dynamic theo flow.steps[].levels[]. Comment khi duyệt auto sync sang
|
||||||
// done. Hiện tại Section 5 FE sẽ KHÔNG hiển thị opinion comment per
|
// Section 5 (read-only summary). Empty comment → "(duyệt — không ý kiến)"
|
||||||
// Level — sẽ wire khi Chunk E (FE Section 5 LevelOpinionsV2).
|
// 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
|
// 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.
|
// thẳng Cấp cuối. Admin opt-in per slot tại AllowApproverSkipToFinal.
|
||||||
if (skipToFinal)
|
if (skipToFinal)
|
||||||
{
|
{
|
||||||
var matchingLevel = pendingLevelGroup.FirstOrDefault(l => actorUserId.HasValue && l.ApproverUserId == actorUserId.Value)
|
|
||||||
?? pendingLevelGroup.First();
|
|
||||||
if (!isAdmin && !isSystem && !matchingLevel.AllowApproverSkipToFinal)
|
if (!isAdmin && !isSystem && !matchingLevel.AllowApproverSkipToFinal)
|
||||||
{
|
{
|
||||||
throw new ConflictException(
|
throw new ConflictException(
|
||||||
@ -398,4 +426,18 @@ public class ContractWorkflowService(
|
|||||||
ct: ct);
|
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