[CLAUDE] FE-User FE-Admin: Plan AD — Lịch sử duyệt redesign drop phase badges + next-target hint
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m24s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m24s
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) <noreply@anthropic.com>
This commit is contained in:
@ -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 }) {
|
||||
<ol className="space-y-2">
|
||||
{merged.map(a => {
|
||||
const dec = decisionBadge(a.decision, a.toPhase)
|
||||
const hint = extractNextTargetHint(a.decision, a.toPhase, a.comment)
|
||||
return (
|
||||
<li key={a.id} className="rounded border border-slate-200 bg-white p-3 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn('rounded px-1.5 py-0.5 text-[11px] font-medium', dec.cls)}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap min-w-0">
|
||||
<span className={cn('rounded px-1.5 py-0.5 text-[11px] font-medium shrink-0', dec.cls)}>
|
||||
{dec.label}
|
||||
</span>
|
||||
<span className={cn('rounded px-1.5 py-0.5 text-[11px]', PurchaseEvaluationPhaseColor[a.fromPhase])}>
|
||||
{PurchaseEvaluationPhaseLabel[a.fromPhase]}
|
||||
</span>
|
||||
<span className="text-slate-400">→</span>
|
||||
<span className={cn('rounded px-1.5 py-0.5 text-[11px]', PurchaseEvaluationPhaseColor[a.toPhase])}>
|
||||
{PurchaseEvaluationPhaseLabel[a.toPhase]}
|
||||
</span>
|
||||
{hint && <span className="text-[12px] font-medium text-slate-700">{hint}</span>}
|
||||
</div>
|
||||
<span className="text-xs text-slate-500">{new Date(a.approvedAt).toLocaleString('vi-VN')}</span>
|
||||
<span className="text-xs text-slate-500 shrink-0">{new Date(a.approvedAt).toLocaleString('vi-VN')}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-slate-500">
|
||||
{a.approverName ?? 'Hệ thống'}{a.comment && ` · ${a.comment}`}
|
||||
|
||||
@ -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 }) {
|
||||
<ol className="space-y-2">
|
||||
{merged.map(a => {
|
||||
const dec = decisionBadge(a.decision, a.toPhase)
|
||||
const hint = extractNextTargetHint(a.decision, a.toPhase, a.comment)
|
||||
return (
|
||||
<li key={a.id} className="rounded border border-slate-200 bg-white p-3 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn('rounded px-1.5 py-0.5 text-[11px] font-medium', dec.cls)}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap min-w-0">
|
||||
<span className={cn('rounded px-1.5 py-0.5 text-[11px] font-medium shrink-0', dec.cls)}>
|
||||
{dec.label}
|
||||
</span>
|
||||
<span className={cn('rounded px-1.5 py-0.5 text-[11px]', PurchaseEvaluationPhaseColor[a.fromPhase])}>
|
||||
{PurchaseEvaluationPhaseLabel[a.fromPhase]}
|
||||
</span>
|
||||
<span className="text-slate-400">→</span>
|
||||
<span className={cn('rounded px-1.5 py-0.5 text-[11px]', PurchaseEvaluationPhaseColor[a.toPhase])}>
|
||||
{PurchaseEvaluationPhaseLabel[a.toPhase]}
|
||||
</span>
|
||||
{hint && <span className="text-[12px] font-medium text-slate-700">{hint}</span>}
|
||||
</div>
|
||||
<span className="text-xs text-slate-500">{new Date(a.approvedAt).toLocaleString('vi-VN')}</span>
|
||||
<span className="text-xs text-slate-500 shrink-0">{new Date(a.approvedAt).toLocaleString('vi-VN')}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-slate-500">
|
||||
{a.approverName ?? 'Hệ thống'}{a.comment && ` · ${a.comment}`}
|
||||
|
||||
Reference in New Issue
Block a user