diff --git a/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs b/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs index 3e2df7e..aaac76e 100644 --- a/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs +++ b/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs @@ -4,6 +4,7 @@ using SolutionErp.Application.Common.Exceptions; using SolutionErp.Application.Common.Interfaces; using SolutionErp.Application.Notifications; using SolutionErp.Application.PurchaseEvaluations.Services; +using SolutionErp.Domain.Common; using SolutionErp.Domain.Contracts; using SolutionErp.Domain.Identity; using SolutionErp.Domain.Notifications; @@ -85,6 +86,103 @@ public class PurchaseEvaluationWorkflowService( } } + // ===== 2-stage department approval (Phase 9 — Migration 16) ===== + // Bug fix anh Kiệt: NV duyệt được hết phase. Logic mới: + // - User.DepartmentId != null + KHÔNG admin/system + KHÔNG resume: + // - DeptManager (TPB) → Stage=Confirm trực tiếp + // - CanBypassReview=true → Stage=Confirm + IsBypassed=true + // - Else (NV) → Stage=Review only, BLOCK phase transition cho đến khi TPB confirm + // - Skip với reject + resume + admin + system + actor không thuộc dept. + if (decision == ApprovalDecision.Approve + && targetPhase != PurchaseEvaluationPhase.DangSoanThao + && targetPhase != PurchaseEvaluationPhase.TuChoi + && !isResumingAfterReject + && !isAdmin && !isSystem + && actorUserId is Guid actorUid) + { + var actor = await userManager.FindByIdAsync(actorUid.ToString()); + if (actor?.DepartmentId is Guid deptId) + { + var isManager = actorRoles.Contains(AppRoles.DeptManager); + var canBypass = actor.CanBypassReview; + var stage = (isManager || canBypass) ? ApprovalStage.Confirm : ApprovalStage.Review; + var isBypassed = !isManager && canBypass; + var roleSnapshot = isManager ? "TPB" : (canBypass ? "NV(bypass)" : "NV"); + + // Upsert: 1 row mỗi (PEId, phase, dept, stage). UNIQUE index enforce. + var existing = await db.PurchaseEvaluationDepartmentApprovals + .FirstOrDefaultAsync(a => + a.PurchaseEvaluationId == evaluation.Id + && a.PhaseAtApproval == (int)fromPhase + && a.DepartmentId == deptId + && a.Stage == stage, ct); + if (existing is null) + { + db.PurchaseEvaluationDepartmentApprovals.Add(new PurchaseEvaluationDepartmentApproval + { + PurchaseEvaluationId = evaluation.Id, + PhaseAtApproval = (int)fromPhase, + DepartmentId = deptId, + Stage = stage, + ApproverUserId = actorUid, + ApproverRoleSnapshot = roleSnapshot, + Comment = comment, + ApprovedAt = dateTime.UtcNow, + IsBypassed = isBypassed, + }); + } + else + { + existing.ApproverUserId = actorUid; + existing.ApproverRoleSnapshot = roleSnapshot; + existing.Comment = comment; + existing.ApprovedAt = dateTime.UtcNow; + existing.IsBypassed = isBypassed; + } + + // Check Stage=Confirm tồn tại cho (PEId, fromPhase, deptId) + var hasConfirm = stage == ApprovalStage.Confirm + || await db.PurchaseEvaluationDepartmentApprovals.AnyAsync(a => + a.PurchaseEvaluationId == evaluation.Id + && a.PhaseAtApproval == (int)fromPhase + && a.DepartmentId == deptId + && a.Stage == ApprovalStage.Confirm, ct); + + if (!hasConfirm) + { + // NV review xong, chưa có TPB confirm → BLOCK phase transition. + // Log Approval + Changelog "đã review" để audit. Phase giữ nguyên. + db.PurchaseEvaluationApprovals.Add(new PurchaseEvaluationApproval + { + PurchaseEvaluationId = evaluation.Id, + FromPhase = fromPhase, + ToPhase = fromPhase, // không đổi phase + ApproverUserId = actorUid, + Decision = ApprovalDecision.Approve, + Comment = $"[Review NV] {comment ?? ""}", + ApprovedAt = dateTime.UtcNow, + }); + + string? reviewerName = (actor.FullName ?? actor.Email); + db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog + { + PurchaseEvaluationId = evaluation.Id, + EntityType = PurchaseEvaluationEntityType.Workflow, + Action = ChangelogAction.Transition, + PhaseAtChange = fromPhase, + UserId = actorUid, + UserName = reviewerName ?? "Hệ thống", + Summary = $"{reviewerName} (NV) đã review phase {fromPhase}, chờ TPB confirm", + ContextNote = comment, + }); + + // TODO Chunk E: notify TPB cùng dept để confirm. + await db.SaveChangesAsync(ct); + return; + } + } + } + evaluation.SlaWarningSent = false; evaluation.Phase = targetPhase;