From 0aaf2df04af76bc1153b439db2c8ce8035e14de4 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Tue, 19 May 2026 11:19:43 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20FE-User=20FE-Admin:=20Plan=20AD=20?= =?UTF-8?q?=E2=80=94=20L=E1=BB=8Bch=20s=E1=BB=AD=20duy=E1=BB=87t=20redesig?= =?UTF-8?q?n=20drop=20phase=20badges=20+=20next-target=20hint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bro UAT 2026-05-19 post-Plan AC2 deploy: phase badges "Đã gửi duyệt → Đã gửi duyệt" gây nhầm (Reject event nhìn giống Approve, không rõ gửi duyệt cho ai). Fix Plan AD — Option A bro chốt: 1. DROP fromPhase→toPhase badges entirely khỏi ApprovalsTab — redundant visual noise khi 3/4 mode return giữ Phase=ChoDuyet, và misleading cho user thấy "Đã gửi duyệt → Đã gửi duyệt" lặp lại. 2. ADD next-target hint parse từ comment via helper extractNextTargetHint(): Approve patterns: - Comment "sang Cấp X" → "→ Cấp X" - Comment "sang Bước X" → "→ Bước X (Cấp 1)" - Comment "[Duyệt vượt cấp tới Cấp cuối]" → "→ Vượt cấp tới Cấp cuối" - toPhase=DaDuyet(20) → "→ Đã duyệt hoàn tất" Reject patterns: - Comment "không lùi được" → "→ Không lùi được" - Comment "Người chỉ định" + Bước/Cấp → "→ Trả về Người chỉ định (Bước X Cấp Y)" - Comment "Người soạn thảo"/"Drafter" → "→ Trả về Người soạn thảo" - Comment "Trả về 1 Cấp"/"Trả về Cấp X" → "→ Lùi về Cấp X" / "→ Lùi 1 Cấp" - Comment "Trả về 1 Bước"/"Trả về Bước X" → "→ Lùi về Bước X" / "→ Lùi 1 Bước" - toPhase=TuChoi(99) → "→ Từ chối hoàn toàn" 3. Layout cleaner: [Decision badge] [Next-target hint] flex-wrap min-w-0 + timestamp shrink-0 right. Comment + actor stays below. 4. Cleanup import: drop unused PurchaseEvaluationPhaseColor (no longer needed after dropping phase badges). Keep PurchaseEvaluationPhaseLabel (still used at line 157+ for InfoTab phase label). Mirror 2 app §3.9 identical logic. Verify: - npm build × fe-user + fe-admin PASS 0 TS err - BE/test unchanged from 25837b6 Co-Authored-By: Claude Opus 4.7 (1M context) --- fe-admin/src/components/pe/PeDetailTabs.tsx | 66 ++++++++++++++++----- fe-user/src/components/pe/PeDetailTabs.tsx | 66 ++++++++++++++++----- 2 files changed, 104 insertions(+), 28 deletions(-) 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}`}