[CLAUDE] PE-Workflow: UAT S22+1 — disable cả 3 button khi không quyền + BE guard
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m29s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m29s
User UAT feedback: "Nếu đã không được quyền thao tác thì ko được quyền thao tác hết tất cả các hành động" — trước đây chỉ "Duyệt" disabled, "Trả lại" + "Từ chối" vẫn enabled (design intent S17 cũ). FE 2 app mirror (PeWorkflowPanel.tsx): - `isDisabled = blockedByV2Level` (drop `isForwardApprove &&` qualifier) - Tooltip update "mới thao tác được (Duyệt / Trả lại / Từ chối)" - Comment refresh ghi UAT S22+1 spec + cross-ref BE EnsureCanRejectV2Async BE defense-in-depth (PurchaseEvaluationWorkflowService.cs): - Helper mới `EnsureCanRejectV2Async` mirror FE actorInV2Level logic: Skip silent khi admin/V1/non-ChoDuyet/no actor/no pointer. Throw ForbiddenException khi V2 + ChoDuyet + actor != currentLevel.ApproverUserId. - Invoke ở top Reject branch (cover cả TuChoi + Trả lại sub-branches). - Chặn request forge: non-approver gọi PATCH /transitions direct sẽ 403. Test (test-before §7 — security guard critical algorithm): - ReturnMode tests existing 7/7 vẫn PASS (a2.Id = currentLevel approver, guard accept) - +1 NEW test `Reject_NonApprover_V2_Throws_ForbiddenException` — outsider Drafter role gọi Reject phiếu V2 → throw + Phase không mutate Verify: - dotnet test SolutionErp.slnx — 104/104 PASS (+1 guard regression) Δ: 103 → 104 - npm run build × 2 app — pass (482ms + 583ms) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -49,9 +49,10 @@ export function PeWorkflowPanel({
|
||||
.filter((v, i, arr) => arr.findIndex(x => x.userId === v.userId) === i)
|
||||
|
||||
// Mig 24 — V2 schema chỉ cho phép approver trong CurrentApproval.approvers
|
||||
// duyệt cấp hiện tại. Nếu actor không khớp → disable nút "Duyệt forward"
|
||||
// (Trả lại / Từ chối vẫn enabled vì Service không kiểm Bước/Cấp với 2
|
||||
// hành động này — Approver có thể reject bất cứ lúc nào trong phiên).
|
||||
// thao tác cấp hiện tại. UAT S22+1 feedback: "không quyền thao tác = ko quyền
|
||||
// mọi hành động" — disable cả 3 button (Duyệt + Trả lại + Từ chối) khi actor
|
||||
// không match currentLevel.ApproverUserId. BE mirror guard trong
|
||||
// EnsureCanRejectV2Async (defense-in-depth — UI disable + BE reject).
|
||||
// Admin bypass.
|
||||
const v2Approvers = evaluation.currentApproval?.approvers ?? []
|
||||
const actorInV2Level = isAdmin
|
||||
@ -239,11 +240,12 @@ export function PeWorkflowPanel({
|
||||
&& evaluation.phase !== PurchaseEvaluationPhase.TraLai
|
||||
const isCancel = p === PurchaseEvaluationPhase.TuChoi
|
||||
const isForwardApprove = !isSendBack && !isCancel
|
||||
// Mig 24 — disable Duyệt forward nếu V2 pin + actor không trong cấp hiện tại
|
||||
const isDisabled = isForwardApprove && blockedByV2Level
|
||||
// Mig 24 + UAT S22+1 — disable cả 3 button khi actor không match
|
||||
// currentLevel.ApproverUserId. "Không quyền = ko quyền mọi hành động."
|
||||
const isDisabled = blockedByV2Level
|
||||
const label = isSendBack ? '← Trả lại' : isCancel ? '✗ Từ chối' : '✓ Duyệt'
|
||||
const title = isDisabled && evaluation.currentApproval
|
||||
? `Cấp ${evaluation.currentApproval.levelOrder} chỉ ${evaluation.currentApproval.approvers.map(a => a.fullName).join(' / ')} mới duyệt được.`
|
||||
? `Cấp ${evaluation.currentApproval.levelOrder} chỉ ${evaluation.currentApproval.approvers.map(a => a.fullName).join(' / ')} mới thao tác được (Duyệt / Trả lại / Từ chối).`
|
||||
: isForwardApprove
|
||||
? `Duyệt → ${PurchaseEvaluationPhaseLabel[p]}`
|
||||
: undefined
|
||||
|
||||
Reference in New Issue
Block a user