[CLAUDE] FE-PE: Chunk B — Fix button "Trả lại" gửi decision=Approve thay vì Reject (gotcha #45) mirror 2 app

Bug pattern: button "← Trả lại" trong PeWorkflowPanel.tsx hiển thị đúng label
(L205-207 isSendBack include TraLai) NHƯNG payload `isReject` (L64-66) thiếu
nhánh TraLai → gửi decision=1 (Approve) thay vì 2 (Reject) khi target=TraLai
(98). BE Service vào APPROVE STEP → ApproveV2Async UPSERT opinion "đã duyệt"
+ advance Cấp tiếp theo. User UAT thấy: "Trả về nhưng hệ thống vẫn duyệt".

Inconsistency thứ 2: dialog `isSendBack` (L247-248) cũng thiếu nhánh TraLai
→ dialog title fallback `✓ Duyệt → Trả lại` + KHÔNG hiển thị amber warning.

Fix 3 chỗ × 2 app (fe-user + fe-admin, rule §3.9 mirror):
1. `isReject` payload — thêm nhánh `target=TraLai && phase!=TraLai`
2. dialog `isSendBack` — thêm nhánh TraLai + guard phase != TraLai
3. Comments document context bug + cross-ref BE guard Chunk A

Sync với BE guard (Chunk A `de00887` `PurchaseEvaluationWorkflowService.cs`):
- BE throw ConflictException khi target ∈ {TraLai, TuChoi} && decision != Reject
- 2 phía cùng đúng → no payload mismatch

Verify:
- npm run build × 2 app pass (fe-user 17.91s, fe-admin 6.71s, 0 TS6 err)
- Warning chunk size pre-existing (NOT introduced)

Pending Chunk C: docs gotcha #45 + STATUS + HANDOFF + session log.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-13 09:43:20 +07:00
parent de0088742f
commit 4b29d00716
2 changed files with 28 additions and 4 deletions

View File

@ -63,11 +63,18 @@ export function PeWorkflowPanel({
mutationFn: async () => { mutationFn: async () => {
// Decision = Reject (2) khi: // Decision = Reject (2) khi:
// - target = TuChoi (huỷ phiếu) // - target = TuChoi (huỷ phiếu)
// - target = DangSoanThao từ phase trung gian (= Trả lại — smart reject Mig 16 // - target = DangSoanThao từ phase trung gian (= Trả lại legacy Mig 16
// set RejectedFromPhase + clear N-stage rows + Drafter resume jump-back) // set RejectedFromPhase + clear N-stage rows + Drafter resume jump-back)
// - target = TraLai (98) từ phase trung gian — Session 17 spec mới: Trả
// lại là Phase RIÊNG (gotcha #45 — thiếu nhánh này gây "Trả về nhưng
// hệ thống vẫn duyệt" do BE nhận decision=Approve → ApproveV2Async).
// BE có guard mirror trong PurchaseEvaluationWorkflowService.TransitionAsync
// throw ConflictException nếu payload mismatch — phải sync 2 phía.
const isReject = target === PurchaseEvaluationPhase.TuChoi const isReject = target === PurchaseEvaluationPhase.TuChoi
|| (target === PurchaseEvaluationPhase.DangSoanThao || (target === PurchaseEvaluationPhase.DangSoanThao
&& evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao) && evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao)
|| (target === PurchaseEvaluationPhase.TraLai
&& evaluation.phase !== PurchaseEvaluationPhase.TraLai)
return api.post(`/purchase-evaluations/${evaluation.id}/transitions`, { return api.post(`/purchase-evaluations/${evaluation.id}/transitions`, {
targetPhase: target, targetPhase: target,
decision: isReject ? 2 : 1, decision: isReject ? 2 : 1,
@ -250,8 +257,13 @@ export function PeWorkflowPanel({
{target !== null && (() => { {target !== null && (() => {
const isCancel = target === PurchaseEvaluationPhase.TuChoi const isCancel = target === PurchaseEvaluationPhase.TuChoi
const isSendBack = target === PurchaseEvaluationPhase.DangSoanThao // isSendBack sync với button label + payload isReject (gotcha #45).
// Include cả DangSoanThao (legacy Mig 16) lẫn TraLai (Session 17 spec)
// — cả 2 là Trả lại Drafter sửa.
const isSendBack = (target === PurchaseEvaluationPhase.DangSoanThao
|| target === PurchaseEvaluationPhase.TraLai)
&& evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao && evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao
&& evaluation.phase !== PurchaseEvaluationPhase.TraLai
const dialogTitle = isCancel const dialogTitle = isCancel
? '✗ Từ chối phiếu (khoá hoàn toàn)' ? '✗ Từ chối phiếu (khoá hoàn toàn)'
: isSendBack : isSendBack

View File

@ -59,11 +59,18 @@ export function PeWorkflowPanel({
mutationFn: async () => { mutationFn: async () => {
// Decision = Reject (2) khi: // Decision = Reject (2) khi:
// - target = TuChoi (huỷ phiếu) // - target = TuChoi (huỷ phiếu)
// - target = DangSoanThao từ phase trung gian (= Trả lại — smart reject Mig 16 // - target = DangSoanThao từ phase trung gian (= Trả lại legacy Mig 16
// set RejectedFromPhase + clear N-stage rows + Drafter resume jump-back) // set RejectedFromPhase + clear N-stage rows + Drafter resume jump-back)
// - target = TraLai (98) từ phase trung gian — Session 17 spec mới: Trả
// lại là Phase RIÊNG (gotcha #45 — thiếu nhánh này gây "Trả về nhưng
// hệ thống vẫn duyệt" do BE nhận decision=Approve → ApproveV2Async).
// BE có guard mirror trong PurchaseEvaluationWorkflowService.TransitionAsync
// throw ConflictException nếu payload mismatch — phải sync 2 phía.
const isReject = target === PurchaseEvaluationPhase.TuChoi const isReject = target === PurchaseEvaluationPhase.TuChoi
|| (target === PurchaseEvaluationPhase.DangSoanThao || (target === PurchaseEvaluationPhase.DangSoanThao
&& evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao) && evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao)
|| (target === PurchaseEvaluationPhase.TraLai
&& evaluation.phase !== PurchaseEvaluationPhase.TraLai)
return api.post(`/purchase-evaluations/${evaluation.id}/transitions`, { return api.post(`/purchase-evaluations/${evaluation.id}/transitions`, {
targetPhase: target, targetPhase: target,
decision: isReject ? 2 : 1, decision: isReject ? 2 : 1,
@ -244,8 +251,13 @@ export function PeWorkflowPanel({
{target !== null && (() => { {target !== null && (() => {
const isCancel = target === PurchaseEvaluationPhase.TuChoi const isCancel = target === PurchaseEvaluationPhase.TuChoi
const isSendBack = target === PurchaseEvaluationPhase.DangSoanThao // isSendBack sync với button label L205-207 + payload isReject L64-68
// (gotcha #45). Include cả DangSoanThao (legacy Mig 16) lẫn TraLai
// (Session 17 spec) — cả 2 là Trả lại Drafter sửa.
const isSendBack = (target === PurchaseEvaluationPhase.DangSoanThao
|| target === PurchaseEvaluationPhase.TraLai)
&& evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao && evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao
&& evaluation.phase !== PurchaseEvaluationPhase.TraLai
const dialogTitle = isCancel const dialogTitle = isCancel
? '✗ Từ chối phiếu (khoá hoàn toàn)' ? '✗ Từ chối phiếu (khoá hoàn toàn)'
: isSendBack : isSendBack