From a734bf2b8bf5bae31424724238a9a1fe737d3ab8 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Tue, 19 May 2026 10:45:25 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20PurchaseEvaluation:=20Plan=20AC=20?= =?UTF-8?q?=E2=80=94=20fix=20L=E1=BB=8Bch=20s=E1=BB=AD=20duy=E1=BB=87t=20p?= =?UTF-8?q?anel=20show=20Tr=E1=BA=A3=20l=E1=BA=A1i=20+=20Duy=E1=BB=87t=20v?= =?UTF-8?q?=C6=B0=E1=BB=A3t=20c=E1=BA=A5p?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bro UAT 2026-05-19 screenshot: panel "Lịch sử duyệt" KHÔNG show Return mode events (Bro Trả lại từ Phan Văn Chương → Trà missing) + KHÔNG distinct event Duyệt vượt cấp (skipToFinal F2). Root cause: - PurchaseEvaluationApprovals.Add() chỉ ở Approve branch (line 472 V2 + 660 V1) - Reject branch line 75-103 NEVER adds Approval row — chỉ log Changelog - skipToFinal advance branch line 532-572 dùng existing line 472 row nhưng comment KHÔNG distinct "vượt cấp" semantic vs approve thường Fix Plan AC: 1. BE Service.cs Reject branch (line 75-103): capture pre-call Step/Level trước ApplyReturnModeAsync mutate pointer, add Approval row sau khi mutate: Decision=Reject + FromPhase + ToPhase=evaluation.Phase + Comment carry from-position + mode summary. Cover cả Trả lại (TraLai+pointer-mode) + Từ chối (TuChoi terminal). 2. BE Service.cs line 472 Approve branch: enrich Comment với prefix "[Duyệt vượt cấp tới Cấp cuối]" khi skipToFinal=true để Lịch sử duyệt distinguish vượt cấp với approve thường. 3. FE PeDetailTabs.tsx × 2 app ApprovalsTab: add Decision badge phân biệt Approve (emerald) / Trả lại (amber) / Từ chối (rose). Vì 3/4 mode Trả lại (OneLevel/OneStep/Assignee) giữ Phase=ChoDuyet → fromPhase→toPhase badge giống Approve. Decision badge bù visual phân biệt. Verify: - dotnet build clean 0 err 2 warn (pre-existing DocxRenderer) - dotnet test 111/111 PASS - npm build × fe-user + fe-admin PASS 0 TS err Co-Authored-By: Claude Opus 4.7 (1M context) --- fe-admin/src/components/pe/PeDetailTabs.tsx | 55 +++++++++++++------ fe-user/src/components/pe/PeDetailTabs.tsx | 55 +++++++++++++------ .../PurchaseEvaluationWorkflowService.cs | 33 ++++++++++- 3 files changed, 105 insertions(+), 38 deletions(-) diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index e73669b..b642435 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/src/components/pe/PeDetailTabs.tsx @@ -1999,29 +1999,48 @@ function QuoteDialog({ } // ===== Tab: Duyệt ===== +// Plan AC S25 Bug 3 — Decision badge phân biệt Approve / Trả lại / Từ chối. +// 3/4 mode Trả lại (OneLevel/OneStep/Assignee) giữ Phase=ChoDuyet → fromPhase +// + toPhase badge giống hệt Approve. Decision badge bù visual phân biệt. +const PE_DECISION_REJECT = 2 +function decisionBadge(decision: number, toPhase: number): { label: string; cls: string } { + if (decision === PE_DECISION_REJECT) { + // Reject phân biệt: TuChoi(99) = "Từ chối" / TraLai(98) hoặc ChoDuyet(10) = "Trả lại" + if (toPhase === 99) return { label: 'Từ chối', cls: 'bg-rose-100 text-rose-700 border border-rose-200' } + return { label: 'Trả lại', cls: 'bg-amber-100 text-amber-700 border border-amber-200' } + } + return { label: 'Duyệt', cls: 'bg-emerald-100 text-emerald-700 border border-emerald-200' } +} + function ApprovalsTab({ ev }: { ev: PeDetailBundle }) { if (ev.approvals.length === 0) return

Chưa có bước duyệt nào.

return (
    - {ev.approvals.map(a => ( -
  1. -
    -
    - - {PurchaseEvaluationPhaseLabel[a.fromPhase]} - - - - {PurchaseEvaluationPhaseLabel[a.toPhase]} - + {ev.approvals.map(a => { + const dec = decisionBadge(a.decision, a.toPhase) + return ( +
  2. +
    +
    + + {dec.label} + + + {PurchaseEvaluationPhaseLabel[a.fromPhase]} + + + + {PurchaseEvaluationPhaseLabel[a.toPhase]} + +
    + {new Date(a.approvedAt).toLocaleString('vi-VN')}
    - {new Date(a.approvedAt).toLocaleString('vi-VN')} - -
    - {a.approverName ?? 'Hệ thống'}{a.comment && ` · ${a.comment}`} -
    -
  3. - ))} +
    + {a.approverName ?? 'Hệ thống'}{a.comment && ` · ${a.comment}`} +
    + + ) + })}
) } diff --git a/fe-user/src/components/pe/PeDetailTabs.tsx b/fe-user/src/components/pe/PeDetailTabs.tsx index 4413698..84a22cf 100644 --- a/fe-user/src/components/pe/PeDetailTabs.tsx +++ b/fe-user/src/components/pe/PeDetailTabs.tsx @@ -1993,29 +1993,48 @@ function QuoteDialog({ } // ===== Tab: Duyệt ===== +// Plan AC S25 Bug 3 — Decision badge phân biệt Approve / Trả lại / Từ chối. +// 3/4 mode Trả lại (OneLevel/OneStep/Assignee) giữ Phase=ChoDuyet → fromPhase +// + toPhase badge giống hệt Approve. Decision badge bù visual phân biệt. +const PE_DECISION_REJECT = 2 +function decisionBadge(decision: number, toPhase: number): { label: string; cls: string } { + if (decision === PE_DECISION_REJECT) { + // Reject phân biệt: TuChoi(99) = "Từ chối" / TraLai(98) hoặc ChoDuyet(10) = "Trả lại" + if (toPhase === 99) return { label: 'Từ chối', cls: 'bg-rose-100 text-rose-700 border border-rose-200' } + return { label: 'Trả lại', cls: 'bg-amber-100 text-amber-700 border border-amber-200' } + } + return { label: 'Duyệt', cls: 'bg-emerald-100 text-emerald-700 border border-emerald-200' } +} + function ApprovalsTab({ ev }: { ev: PeDetailBundle }) { if (ev.approvals.length === 0) return

Chưa có bước duyệt nào.

return (
    - {ev.approvals.map(a => ( -
  1. -
    -
    - - {PurchaseEvaluationPhaseLabel[a.fromPhase]} - - - - {PurchaseEvaluationPhaseLabel[a.toPhase]} - + {ev.approvals.map(a => { + const dec = decisionBadge(a.decision, a.toPhase) + return ( +
  2. +
    +
    + + {dec.label} + + + {PurchaseEvaluationPhaseLabel[a.fromPhase]} + + + + {PurchaseEvaluationPhaseLabel[a.toPhase]} + +
    + {new Date(a.approvedAt).toLocaleString('vi-VN')}
    - {new Date(a.approvedAt).toLocaleString('vi-VN')} - -
    - {a.approverName ?? 'Hệ thống'}{a.comment && ` · ${a.comment}`} -
    -
  3. - ))} +
    + {a.approverName ?? 'Hệ thống'}{a.comment && ` · ${a.comment}`} +
    + + ) + })}
) } diff --git a/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs b/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs index 361b896..5fad08b 100644 --- a/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs +++ b/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs @@ -80,6 +80,13 @@ public class PurchaseEvaluationWorkflowService( // guard tránh request forge non-approver gọi PATCH direct. await EnsureCanRejectV2Async(evaluation, actorUserId, isAdmin, ct); + // Plan AC S25 Bug 3a — capture pre-call Step/Level để log Approval row + // chính xác (ApplyReturnModeAsync mutate pointer cho 3 mode OneLevel/ + // OneStep/Assignee). FE ApprovalsTab render `ev.approvals` — trước + // đây Reject KHÔNG add row → Lịch sử duyệt panel mất event Trả lại. + var fromStepIdx = evaluation.CurrentWorkflowStepIndex; + var fromLevelOrder = evaluation.CurrentApprovalLevelOrder; + if (targetPhase == PurchaseEvaluationPhase.TuChoi) { // Từ chối hoàn toàn — phiếu khoá vĩnh viễn (lock edit Mig 16). @@ -97,6 +104,25 @@ public class PurchaseEvaluationWorkflowService( ? returnSummary : $"{comment} [{returnSummary}]"; } + + // Plan AC S25 Bug 3a — add Approval row cho Lịch sử duyệt panel + // (FE ApprovalsTab). Decision=Reject + Comment carry mode + summary. + // From-position dùng pre-call Step/Level để show actor đã trả lại + // từ Bước/Cấp nào. + var fromPos = fromStepIdx.HasValue && fromLevelOrder.HasValue + ? $"[Bước {fromStepIdx.Value + 1} — Cấp {fromLevelOrder.Value}] " + : ""; + db.PurchaseEvaluationApprovals.Add(new PurchaseEvaluationApproval + { + PurchaseEvaluationId = evaluation.Id, + FromPhase = fromPhase, + ToPhase = evaluation.Phase, + ApproverUserId = actorUserId, + Decision = ApprovalDecision.Reject, + Comment = $"{fromPos}{comment ?? ""}".Trim(), + ApprovedAt = dateTime.UtcNow, + }); + await LogTransitionAsync(evaluation, fromPhase, evaluation.Phase, actorUserId, decision, comment, ct); await db.SaveChangesAsync(ct); return; @@ -468,7 +494,10 @@ public class PurchaseEvaluationWorkflowService( } } - // Log approval + // Log approval. Plan AC S25 Bug 3b — enrich comment với prefix + // "[Duyệt vượt cấp]" khi skipToFinal=true để FE Lịch sử duyệt panel + // phân biệt rõ event vượt cấp với approve thường. + var skipPrefix = skipToFinal ? "[Duyệt vượt cấp tới Cấp cuối] " : ""; db.PurchaseEvaluationApprovals.Add(new PurchaseEvaluationApproval { PurchaseEvaluationId = evaluation.Id, @@ -476,7 +505,7 @@ public class PurchaseEvaluationWorkflowService( ToPhase = evaluation.Phase, ApproverUserId = actorUserId, Decision = ApprovalDecision.Approve, - Comment = $"[Bước {currentIdx + 1} — Cấp {currentLevelOrder}] {comment ?? ""}", + Comment = $"{skipPrefix}[Bước {currentIdx + 1} — Cấp {currentLevelOrder}] {comment ?? ""}".Trim(), ApprovedAt = dateTime.UtcNow, });