diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx
index 59b5424..216aed1 100644
--- a/fe-admin/src/components/pe/PeDetailTabs.tsx
+++ b/fe-admin/src/components/pe/PeDetailTabs.tsx
@@ -25,7 +25,6 @@ import {
PeDisplayStatusColor,
PeDisplayStatusLabel,
PurchaseEvaluationPhase,
- PurchaseEvaluationPhaseColor,
PurchaseEvaluationPhaseLabel,
PurchaseEvaluationTypeLabel,
getPeDisplayStatus,
@@ -2001,8 +2000,8 @@ 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.
+// Plan AD S25 — Drop fromPhase→toPhase badges (gây nhầm khi cùng ChoDuyet);
+// thay bằng next-target hint parse từ comment để rõ "gửi duyệt cho ai / trả về đâu".
const PE_DECISION_REJECT = 2
function decisionBadge(decision: number, toPhase: number): { label: string; cls: string } {
if (decision === PE_DECISION_REJECT) {
@@ -2013,6 +2012,50 @@ function decisionBadge(decision: number, toPhase: number): { label: string; cls:
return { label: 'Duyệt', cls: 'bg-emerald-100 text-emerald-700 border border-emerald-200' }
}
+// Plan AD S25 — Parse comment để show next-target hint rõ ràng. BE comment
+// format chuẩn từ Service:
+// Approve advance Cấp: "Hoàn tất Cấp X, sang Cấp Y cùng Bước Z"
+// Approve advance Bước: "Hoàn tất Bước X/Y, sang Bước Z (Cấp 1)"
+// Approve skipToFinal: "[Duyệt vượt cấp tới Cấp cuối] ..." (Plan AC)
+// Approve terminal: toPhase=DaDuyet(20)
+// Reject OneLevel: "Trả về Cấp X (cùng Bước Y)" hoặc "không lùi được"
+// Reject OneStep: "Trả về Bước X Cấp Y" hoặc "không lùi được"
+// Reject Assignee: "Trả về Người chỉ định — Bước X (...) Cấp Y"
+// Reject Drafter: "Trả về Người soạn thảo"
+// Reject TuChoi: toPhase=TuChoi(99)
+function extractNextTargetHint(decision: number, toPhase: number, comment: string | null): string {
+ if (decision === PE_DECISION_REJECT) {
+ if (toPhase === 99) return '→ Từ chối hoàn toàn'
+ const c = comment ?? ''
+ if (c.includes('không lùi được')) return '→ Không lùi được'
+ if (c.includes('Người chỉ định')) {
+ const m = c.match(/Bước\s*(\d+).*?Cấp\s*(\d+)/)
+ return m ? `→ Trả về Người chỉ định (Bước ${m[1]} Cấp ${m[2]})` : '→ Trả về Người chỉ định'
+ }
+ if (c.includes('Người soạn thảo') || c.includes('Drafter')) return '→ Trả về Người soạn thảo'
+ if (c.includes('Trả về 1 Cấp') || c.includes('Trả về Cấp')) {
+ const m = c.match(/Cấp\s*(\d+)/)
+ return m ? `→ Lùi về Cấp ${m[1]}` : '→ Lùi 1 Cấp'
+ }
+ if (c.includes('Trả về 1 Bước') || c.includes('Trả về Bước')) {
+ const m = c.match(/Bước\s*(\d+)/)
+ return m ? `→ Lùi về Bước ${m[1]}` : '→ Lùi 1 Bước'
+ }
+ return ''
+ }
+ // Approve
+ if (toPhase === 20) return '→ Đã duyệt hoàn tất'
+ const c = comment ?? ''
+ if (c.includes('Duyệt vượt cấp') || c.includes('Approver skip thẳng tới')) {
+ return '→ Vượt cấp tới Cấp cuối'
+ }
+ const levelMatch = c.match(/sang Cấp\s*(\d+)/)
+ if (levelMatch) return `→ Cấp ${levelMatch[1]}`
+ const stepMatch = c.match(/sang Bước\s*(\d+)/)
+ if (stepMatch) return `→ Bước ${stepMatch[1]} (Cấp 1)`
+ return ''
+}
+
function ApprovalsTab({ ev }: { ev: PeDetailBundle }) {
// Plan AC2 S25 — FE merge view: fetch changelogs + reconstruct synthetic
// Reject rows từ pre-Plan AC historical data (PE cũ deploy trước 2026-05-19
@@ -2070,22 +2113,17 @@ function ApprovalsTab({ ev }: { ev: PeDetailBundle }) {
{merged.map(a => {
const dec = decisionBadge(a.decision, a.toPhase)
+ const hint = extractNextTargetHint(a.decision, a.toPhase, a.comment)
return (
-
-
-
-
+
+
+
{dec.label}
-
- {PurchaseEvaluationPhaseLabel[a.fromPhase]}
-
- →
-
- {PurchaseEvaluationPhaseLabel[a.toPhase]}
-
+ {hint && {hint}}
-
{new Date(a.approvedAt).toLocaleString('vi-VN')}
+
{new Date(a.approvedAt).toLocaleString('vi-VN')}
{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 37c4f19..e87e3a8 100644
--- a/fe-user/src/components/pe/PeDetailTabs.tsx
+++ b/fe-user/src/components/pe/PeDetailTabs.tsx
@@ -25,7 +25,6 @@ import {
PeDisplayStatusColor,
PeDisplayStatusLabel,
PurchaseEvaluationPhase,
- PurchaseEvaluationPhaseColor,
PurchaseEvaluationPhaseLabel,
PurchaseEvaluationTypeLabel,
getPeDisplayStatus,
@@ -1995,8 +1994,8 @@ 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.
+// Plan AD S25 — Drop fromPhase→toPhase badges (gây nhầm khi cùng ChoDuyet);
+// thay bằng next-target hint parse từ comment để rõ "gửi duyệt cho ai / trả về đâu".
const PE_DECISION_REJECT = 2
function decisionBadge(decision: number, toPhase: number): { label: string; cls: string } {
if (decision === PE_DECISION_REJECT) {
@@ -2007,6 +2006,50 @@ function decisionBadge(decision: number, toPhase: number): { label: string; cls:
return { label: 'Duyệt', cls: 'bg-emerald-100 text-emerald-700 border border-emerald-200' }
}
+// Plan AD S25 — Parse comment để show next-target hint rõ ràng. BE comment
+// format chuẩn từ Service:
+// Approve advance Cấp: "Hoàn tất Cấp X, sang Cấp Y cùng Bước Z"
+// Approve advance Bước: "Hoàn tất Bước X/Y, sang Bước Z (Cấp 1)"
+// Approve skipToFinal: "[Duyệt vượt cấp tới Cấp cuối] ..." (Plan AC)
+// Approve terminal: toPhase=DaDuyet(20)
+// Reject OneLevel: "Trả về Cấp X (cùng Bước Y)" hoặc "không lùi được"
+// Reject OneStep: "Trả về Bước X Cấp Y" hoặc "không lùi được"
+// Reject Assignee: "Trả về Người chỉ định — Bước X (...) Cấp Y"
+// Reject Drafter: "Trả về Người soạn thảo"
+// Reject TuChoi: toPhase=TuChoi(99)
+function extractNextTargetHint(decision: number, toPhase: number, comment: string | null): string {
+ if (decision === PE_DECISION_REJECT) {
+ if (toPhase === 99) return '→ Từ chối hoàn toàn'
+ const c = comment ?? ''
+ if (c.includes('không lùi được')) return '→ Không lùi được'
+ if (c.includes('Người chỉ định')) {
+ const m = c.match(/Bước\s*(\d+).*?Cấp\s*(\d+)/)
+ return m ? `→ Trả về Người chỉ định (Bước ${m[1]} Cấp ${m[2]})` : '→ Trả về Người chỉ định'
+ }
+ if (c.includes('Người soạn thảo') || c.includes('Drafter')) return '→ Trả về Người soạn thảo'
+ if (c.includes('Trả về 1 Cấp') || c.includes('Trả về Cấp')) {
+ const m = c.match(/Cấp\s*(\d+)/)
+ return m ? `→ Lùi về Cấp ${m[1]}` : '→ Lùi 1 Cấp'
+ }
+ if (c.includes('Trả về 1 Bước') || c.includes('Trả về Bước')) {
+ const m = c.match(/Bước\s*(\d+)/)
+ return m ? `→ Lùi về Bước ${m[1]}` : '→ Lùi 1 Bước'
+ }
+ return ''
+ }
+ // Approve
+ if (toPhase === 20) return '→ Đã duyệt hoàn tất'
+ const c = comment ?? ''
+ if (c.includes('Duyệt vượt cấp') || c.includes('Approver skip thẳng tới')) {
+ return '→ Vượt cấp tới Cấp cuối'
+ }
+ const levelMatch = c.match(/sang Cấp\s*(\d+)/)
+ if (levelMatch) return `→ Cấp ${levelMatch[1]}`
+ const stepMatch = c.match(/sang Bước\s*(\d+)/)
+ if (stepMatch) return `→ Bước ${stepMatch[1]} (Cấp 1)`
+ return ''
+}
+
function ApprovalsTab({ ev }: { ev: PeDetailBundle }) {
// Plan AC2 S25 — FE merge view: fetch changelogs + reconstruct synthetic
// Reject rows từ pre-Plan AC historical data (PE cũ deploy trước 2026-05-19
@@ -2064,22 +2107,17 @@ function ApprovalsTab({ ev }: { ev: PeDetailBundle }) {
{merged.map(a => {
const dec = decisionBadge(a.decision, a.toPhase)
+ const hint = extractNextTargetHint(a.decision, a.toPhase, a.comment)
return (
-
-
-
-
+
+
+
{dec.label}
-
- {PurchaseEvaluationPhaseLabel[a.fromPhase]}
-
- →
-
- {PurchaseEvaluationPhaseLabel[a.toPhase]}
-
+ {hint && {hint}}
-
{new Date(a.approvedAt).toLocaleString('vi-VN')}
+
{new Date(a.approvedAt).toLocaleString('vi-VN')}
{a.approverName ?? 'Hệ thống'}{a.comment && ` · ${a.comment}`}