diff --git a/fe-admin/src/components/pe/PeWorkflowPanel.tsx b/fe-admin/src/components/pe/PeWorkflowPanel.tsx
index ea82b29..85e0014 100644
--- a/fe-admin/src/components/pe/PeWorkflowPanel.tsx
+++ b/fe-admin/src/components/pe/PeWorkflowPanel.tsx
@@ -418,7 +418,7 @@ export function PeWorkflowPanel({
Duyệt thẳng Cấp cuối (skip Bước/Cấp trung gian)
- Phiếu sẽ tiến thẳng tới "Đã duyệt" (terminal) — bỏ qua mọi Cấp/Bước còn lại.
+ Phiếu sẽ skip tới NV cuối (CEO/cấp ký cuối) — NV cuối vẫn cần duyệt thật để hoàn tất.
@@ -426,8 +426,8 @@ export function PeWorkflowPanel({
)}
{!isCancel && !isSendBack && skipToFinalApprover && (
- ⚠ Hành động KHÔNG quay lại được (trừ khi Drafter reset toàn bộ). Phiếu sẽ
- skip qua tất cả Cấp/Bước còn lại và chuyển thẳng "Đã duyệt".
+ ⚠ Bỏ qua mọi Cấp/Bước trung gian, phiếu chuyển thẳng tới NV cuối. NV cuối
+ vẫn phải ký duyệt thật để phiếu thành "Đã duyệt".
)}
diff --git a/fe-user/src/components/pe/PeWorkflowPanel.tsx b/fe-user/src/components/pe/PeWorkflowPanel.tsx
index 82e0239..e724e57 100644
--- a/fe-user/src/components/pe/PeWorkflowPanel.tsx
+++ b/fe-user/src/components/pe/PeWorkflowPanel.tsx
@@ -415,7 +415,7 @@ export function PeWorkflowPanel({
Duyệt thẳng Cấp cuối (skip Bước/Cấp trung gian)
- Phiếu sẽ tiến thẳng tới "Đã duyệt" (terminal) — bỏ qua mọi Cấp/Bước còn lại.
+ Phiếu sẽ skip tới NV cuối (CEO/cấp ký cuối) — NV cuối vẫn cần duyệt thật để hoàn tất.
@@ -423,8 +423,8 @@ export function PeWorkflowPanel({
)}
{!isCancel && !isSendBack && skipToFinalApprover && (
- ⚠ Hành động KHÔNG quay lại được (trừ khi Drafter reset toàn bộ). Phiếu sẽ
- skip qua tất cả Cấp/Bước còn lại và chuyển thẳng "Đã duyệt".
+ ⚠ Bỏ qua mọi Cấp/Bước trung gian, phiếu chuyển thẳng tới NV cuối. NV cuối
+ vẫn phải ký duyệt thật để phiếu thành "Đã duyệt".
)}
diff --git a/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs b/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs
index e642f81..61f146c 100644
--- a/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs
+++ b/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs
@@ -467,13 +467,19 @@ public class PurchaseEvaluationWorkflowService(
existingOpinion.SignedByFullName = actorFullName;
}
- // Mig 31 (S23 t1 Plan K) — F2 Approver scope ChoDuyet: duyệt thẳng Cấp cuối.
- // Admin opt-in per slot tại matchingLevel.AllowApproverSkipToFinal. Khi
- // Approver tick checkbox "Duyệt thẳng Cấp cuối" trong Workspace + admin
- // đã enable flag cho slot này → bỏ qua mọi Bước/Cấp trung gian còn lại,
- // set Phase=DaDuyet terminal trực tiếp. Mirror F3+F4 admin opt-in per-
- // Approver-slot pattern (Mig 29 + Mig 30) reinforced 3× cumulative.
- // Non-admin + flag off → ConflictException. Admin bypass flag.
+ // Mig 31 (S23 t1 Plan K) — F2 Approver scope ChoDuyet: skip thẳng tới
+ // NV cuối (CEO / last approver). Admin opt-in per slot tại
+ // matchingLevel.AllowApproverSkipToFinal. Khi Approver tick checkbox
+ // "Duyệt thẳng Cấp cuối" + admin enable flag → bỏ qua mọi Bước/Cấp
+ // TRUNG GIAN còn lại, advance pointer tới Bước cuối + Cấp cuối (max).
+ // Phase GIỮ NGUYÊN ChoDuyet — NV cuối vẫn cần ký thật để tiến DaDuyet
+ // (KHÔNG auto-approve terminal). Mirror F3+F4 admin opt-in per-slot
+ // pattern (Mig 29 + Mig 30) reinforced 3× cumulative. Non-admin + flag
+ // off → ConflictException. Admin bypass flag.
+ //
+ // S23 t2 spec fix: bro UAT feedback Plan K K2 implement SAI semantic
+ // (set Phase=DaDuyet terminal auto-approve), refactor sang advance
+ // pointer tới Cấp cuối (CEO duyệt thật).
if (skipToFinal)
{
if (!isAdmin && !isSystem && !matchingLevel.AllowApproverSkipToFinal)
@@ -484,19 +490,37 @@ public class PurchaseEvaluationWorkflowService(
"'Duyệt thẳng Cấp cuối' trong Workflow Designer cho slot này.");
}
- evaluation.Phase = PurchaseEvaluationPhase.DaDuyet;
- evaluation.CurrentWorkflowStepIndex = null;
- evaluation.CurrentApprovalLevelOrder = null;
- evaluation.SlaDeadline = null;
- await LogTransitionAsync(
- evaluation,
- PurchaseEvaluationPhase.ChoDuyet,
- PurchaseEvaluationPhase.DaDuyet,
- actorUserId,
- ApprovalDecision.Approve,
- $"[Approver duyệt thẳng Cấp cuối — Bước {currentIdx + 1} Cấp {currentLevelOrder} → DaDuyet] {comment ?? ""}".Trim(),
- ct);
- return;
+ // Resolve last Step + last Level (max LevelOrder trong Step cuối)
+ var lastStepIdx = steps.Count - 1;
+ var lastStep = steps[lastStepIdx];
+ var lastLevelGroups = lastStep.Levels.OrderBy(l => l.Order).GroupBy(l => l.Order).ToList();
+ var lastLevelMaxOrder = lastLevelGroups.Count == 0 ? 1 : lastLevelGroups.Max(g => g.Key);
+
+ // Guard: nếu actor đã ở Cấp cuối Bước cuối thì skipToFinal = no-op
+ // → fall through advance logic bên dưới (normal advance → DaDuyet).
+ if (currentIdx == lastStepIdx && currentLevelOrder == lastLevelMaxOrder)
+ {
+ // No-op skip: actor đã ở slot cuối — fall through normal advance
+ // (sẽ hit branch `nextIdx >= steps.Count` → DaDuyet đúng).
+ }
+ else
+ {
+ // Advance pointer tới Bước cuối + Cấp cuối. Phase giữ ChoDuyet.
+ // CEO/NV cuối thấy phiếu đang chờ duyệt + opinion của actor vừa
+ // ghi sẵn → duyệt cuối để approve DaDuyet thật.
+ evaluation.CurrentWorkflowStepIndex = lastStepIdx;
+ evaluation.CurrentApprovalLevelOrder = lastLevelMaxOrder;
+ evaluation.SlaDeadline = dateTime.UtcNow.AddDays(7);
+ await LogTransitionAsync(
+ evaluation,
+ PurchaseEvaluationPhase.ChoDuyet,
+ PurchaseEvaluationPhase.ChoDuyet,
+ actorUserId,
+ ApprovalDecision.Approve,
+ $"[Approver skip thẳng tới Bước {lastStepIdx + 1} Cấp {lastLevelMaxOrder} (NV cuối) — bỏ qua các Bước/Cấp trung gian] {comment ?? ""}".Trim(),
+ ct);
+ return;
+ }
}
// Advance: nếu còn cấp tiếp trong Step → levelOrder++; else → next Step + level 1