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 (
  1. -
    -
    - +
    +
    + {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 (
    1. -
      -
      - +
      +
      + {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}`}