[CLAUDE] PurchaseEvaluation: Chunk L1 — Fix F2 skipToFinal semantic: skip pointer tới NV cuối (KHÔNG terminate DaDuyet)

Bro UAT S23 t2 catch: Plan K K2 implement F2 SAI semantic — set
Phase=DaDuyet terminal auto-approve. Bro intent: "Duyệt thẳng đến CEO,
bỏ qua các bước khác chứ ko phải chuyển sang đã duyệt."

Refactor Service.cs ApproveV2Async F2 branch:
- Resolve lastStepIdx = steps.Count - 1, lastLevelMaxOrder = max(LevelOrder)
  trong Step cuối
- Advance pointer: CurrentWorkflowStepIndex = lastStepIdx + CurrentApprovalLevelOrder = lastLevelMaxOrder
- Phase GIỮ NGUYÊN ChoDuyet — NV cuối (CEO/last approver) vẫn cần ký thật
  để tiến DaDuyet
- Audit log "Approver skip thẳng tới Bước X Cấp Y (NV cuối) — bỏ qua các Bước/Cấp trung gian"
- Guard no-op: actor đã ở slot cuối → fall through advance logic (normal → DaDuyet)
  (KHÔNG double-advance khi skipToFinal=true ngay slot cuối)
- Reset SLA 7d cho NV cuối nhận lại

FE × 2 app PeWorkflowPanel.tsx (mirror rule §3.9):
- Description text update: "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."
- Amber warning update: "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'."

Verify:
- dotnet build production projects clean (0 err, 2 pre-existing warn)
- npm run build × 2 app pass

Pattern lesson saved memory: Service skipToFinal semantic = advance pointer
NOT terminate. K7 tests TODO update: 3 Approver F2 tests assert pointer
moved to last slot, NOT Phase=DaDuyet. Defer test fix sau UAT confirm UX.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-15 01:39:03 +07:00
parent 409a9676e8
commit f3db9e6cc0
3 changed files with 50 additions and 26 deletions

View File

@ -418,7 +418,7 @@ export function PeWorkflowPanel({
<span>
<span className="font-medium">Duyệt thẳng Cấp cuối (skip Bước/Cấp trung gian)</span>
<span className="mt-0.5 block text-[11px] text-violet-700/80">
Phiếu sẽ tiến thẳng tới "Đã duyệt" (terminal) bỏ qua mọi Cấp/Bước n lại.
Phiếu sẽ skip tới NV cuối (CEO/cấp cuối) NV cuối vẫn cần duyệt thật đ hoàn tất.
</span>
</span>
</label>
@ -426,8 +426,8 @@ export function PeWorkflowPanel({
)}
{!isCancel && !isSendBack && skipToFinalApprover && (
<div className="mb-3 rounded border border-amber-300 bg-amber-50 px-3 py-2 text-[11px] text-amber-800">
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 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 duyệt thật đ phiếu thành "Đã duyệt".
</div>
)}
<Label>Ghi chú (tùy chọn)</Label>

View File

@ -415,7 +415,7 @@ export function PeWorkflowPanel({
<span>
<span className="font-medium">Duyệt thẳng Cấp cuối (skip Bước/Cấp trung gian)</span>
<span className="mt-0.5 block text-[11px] text-violet-700/80">
Phiếu sẽ tiến thẳng tới "Đã duyệt" (terminal) bỏ qua mọi Cấp/Bước n lại.
Phiếu sẽ skip tới NV cuối (CEO/cấp cuối) NV cuối vẫn cần duyệt thật đ hoàn tất.
</span>
</span>
</label>
@ -423,8 +423,8 @@ export function PeWorkflowPanel({
)}
{!isCancel && !isSendBack && skipToFinalApprover && (
<div className="mb-3 rounded border border-amber-300 bg-amber-50 px-3 py-2 text-[11px] text-amber-800">
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 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 duyệt thật đ phiếu thành "Đã duyệt".
</div>
)}
<Label>Ghi chú (tùy chọn)</Label>

View File

@ -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