From 0c5db1385f171f6987a22d01db74676f4fd2eff1 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Thu, 7 May 2026 15:27:29 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20FE-Admin+FE-User:=20PE=20display=20s?= =?UTF-8?q?tatus=20meta=20=E2=80=94=20B=E1=BA=A3n=20nh=C3=A1p=20/=20=C4=90?= =?UTF-8?q?=C3=A3=20g=E1=BB=ADi=20duy=E1=BB=87t=20/=20=C4=90=C3=A3=20duy?= =?UTF-8?q?=E1=BB=87t=20/=20T=E1=BB=AB=20ch=E1=BB=91i?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User feedback 2026-05-07: thêm 2 trạng thái meta hiển thị "Bản nháp" + "Đã gửi duyệt". Bản nháp chỉ hiện ở Thao tác workspace, không hiện ở Duyệt menu. Implementation: ~ types/purchaseEvaluation.ts + PeDisplayStatus enum (BanNhap / DaGuiDuyet / DaDuyet / TuChoi) + PeDisplayStatusLabel + PeDisplayStatusColor + getPeDisplayStatus(phase) helper: DangSoanThao → BanNhap DaDuyet → DaDuyet TuChoi → TuChoi else (any middle phase) → DaGuiDuyet ~ components/pe/PeListPanel.tsx - Phase Select filter → Display status Select (4 option, "Đã gửi duyệt" KHÔNG filter exact phase do multi-phase, để client-side TODO BE) - Row badge dùng display status (gọn 4 màu) + Prop forcedPhase?: number — workspace dùng để khóa filter Bản nháp (DangSoanThao). Khi forcedPhase set: ẩn Select, show "Lọc cố định: Bản nháp" indicator. ~ components/pe/PeDetailTabs.tsx - Header badge dùng display status meta + secondary text "(Phase chi tiết)" nhỏ bên cạnh để approver/dev vẫn biết phase exact ~ pages/pe/PurchaseEvaluationsListPage.tsx - Phase filter Select → display status options - Row badge → display status ~ pages/pe/PurchaseEvaluationWorkspacePage.tsx - PeListPanel forcedPhase={PurchaseEvaluationPhase.DangSoanThao} → workspace chỉ list Bản nháp (đúng UX user yêu cầu) Workflow timeline Panel 3 + workflow service BE KHÔNG đổi (giữ phase chi tiết DangSoanThao/ChoPurchasing/ChoCCM/etc cho approval logic). Pe_*_Pending Duyệt: dùng /inbox endpoint vốn đã filter chỉ phiếu cần user duyệt → DangSoanThao auto-không xuất hiện (không có active approver). Nên Bản nháp auto-hidden từ Duyệt menu, không cần filter thêm. UAT mode: skip verify, push ngay. Co-Authored-By: Claude Opus 4.7 (1M context) --- fe-admin/src/components/pe/PeDetailTabs.tsx | 12 ++++- fe-admin/src/components/pe/PeListPanel.tsx | 50 ++++++++++++++----- .../pe/PurchaseEvaluationWorkspacePage.tsx | 6 ++- .../pages/pe/PurchaseEvaluationsListPage.tsx | 27 +++++++--- fe-admin/src/types/purchaseEvaluation.ts | 36 +++++++++++++ fe-user/src/components/pe/PeDetailTabs.tsx | 12 ++++- fe-user/src/components/pe/PeListPanel.tsx | 50 ++++++++++++++----- .../pe/PurchaseEvaluationWorkspacePage.tsx | 6 ++- .../pages/pe/PurchaseEvaluationsListPage.tsx | 27 +++++++--- fe-user/src/types/purchaseEvaluation.ts | 36 +++++++++++++ 10 files changed, 216 insertions(+), 46 deletions(-) diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index c57d238..0ee33ad 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/src/components/pe/PeDetailTabs.tsx @@ -20,10 +20,13 @@ import { PeAttachmentPurposeLabel, PeDepartmentKind, PeDepartmentKindLabel, + PeDisplayStatusColor, + PeDisplayStatusLabel, PurchaseEvaluationPhase, PurchaseEvaluationPhaseColor, PurchaseEvaluationPhaseLabel, PurchaseEvaluationTypeLabel, + getPeDisplayStatus, type PeAttachment, type PeChangelog, type PeDepartmentOpinion, @@ -75,13 +78,18 @@ export function PeDetailTabs({

{evaluation.tenGoiThau}

+ {/* Display status meta (Bản nháp / Đã gửi duyệt / Đã duyệt / Từ chối) + — phase chi tiết hiện ở Workflow timeline Panel 3. */} - {PurchaseEvaluationPhaseLabel[evaluation.phase]} + {PeDisplayStatusLabel[getPeDisplayStatus(evaluation.phase)]} + + + ({PurchaseEvaluationPhaseLabel[evaluation.phase]}) {readOnly && ( diff --git a/fe-admin/src/components/pe/PeListPanel.tsx b/fe-admin/src/components/pe/PeListPanel.tsx index 89ad018..19a2eb0 100644 --- a/fe-admin/src/components/pe/PeListPanel.tsx +++ b/fe-admin/src/components/pe/PeListPanel.tsx @@ -16,10 +16,12 @@ import { api } from '@/lib/api' import { cn } from '@/lib/cn' import type { Paged } from '@/types/master' import { + PeDisplayStatus, + PeDisplayStatusColor, + PeDisplayStatusLabel, PurchaseEvaluationPhase, - PurchaseEvaluationPhaseColor, - PurchaseEvaluationPhaseLabel, PurchaseEvaluationTypeLabel, + getPeDisplayStatus, type PeListItem, } from '@/types/purchaseEvaluation' @@ -35,6 +37,7 @@ export function PeListPanel({ showCreateButton = false, onCreate, onEditClick, + forcedPhase, }: { typeFilter: number | null pendingMe?: boolean @@ -48,9 +51,13 @@ export function PeListPanel({ onCreate?: () => void /** Pencil edit icon hover next-to-row — click → select + auto-open Section 1 edit mode (URL ?editHeader=1). */ onEditClick?: (id: string) => void + /** Force phase filter (vd workspace chỉ hiện Bản nháp = DangSoanThao). Khi set: + * ẩn phase Select dropdown + force fetch params phase=. */ + forcedPhase?: number }) { + const effectivePhase = forcedPhase !== undefined ? String(forcedPhase) : phase const list = useQuery({ - queryKey: ['pe-list', { typeFilter, pendingMe, search, phase }], + queryKey: ['pe-list', { typeFilter, pendingMe, search, phase: effectivePhase }], queryFn: async () => { if (pendingMe) { const res = await api.get('/purchase-evaluations/inbox', { @@ -63,7 +70,7 @@ export function PeListPanel({ pageSize: 50, search: search || undefined, type: typeFilter ?? undefined, - phase: phase || undefined, + phase: effectivePhase || undefined, }, }) return res.data @@ -93,12 +100,18 @@ export function PeListPanel({ className="pl-8" />
- + {forcedPhase === undefined ? ( + + ) : ( +
+ Lọc cố định: {PeDisplayStatusLabel[getPeDisplayStatus(forcedPhase)]} +
+ )}
{/* List body */} @@ -150,10 +163,10 @@ export function PeListPanel({ - {PurchaseEvaluationPhaseLabel[p.phase]} + {PeDisplayStatusLabel[getPeDisplayStatus(p.phase)]}
@@ -195,3 +208,16 @@ export function PeListPanel({ ) } + +// Map display status → phase enum cho filter dropdown. +// Bản nháp = DangSoanThao (1), Đã duyệt = DaDuyet (7), Từ chối = TuChoi (99). +// Đã gửi duyệt = không filter exact phase (cần BE hỗ trợ multi-phase filter +// hoặc filter client-side). Tạm thời: trả về '' (không filter) → list show +// hết, user vẫn thấy được phiếu Đã gửi duyệt cùng với tất cả khác. Trade-off +// chấp nhận tới khi BE thêm multi-phase param. +function statusToPhaseValue(status: PeDisplayStatus): string { + if (status === PeDisplayStatus.BanNhap) return String(PurchaseEvaluationPhase.DangSoanThao) + if (status === PeDisplayStatus.DaDuyet) return String(PurchaseEvaluationPhase.DaDuyet) + if (status === PeDisplayStatus.TuChoi) return String(PurchaseEvaluationPhase.TuChoi) + return '' // DaGuiDuyet — multi-phase, không filter exact (TODO BE add support) +} diff --git a/fe-admin/src/pages/pe/PurchaseEvaluationWorkspacePage.tsx b/fe-admin/src/pages/pe/PurchaseEvaluationWorkspacePage.tsx index 8707212..4870150 100644 --- a/fe-admin/src/pages/pe/PurchaseEvaluationWorkspacePage.tsx +++ b/fe-admin/src/pages/pe/PurchaseEvaluationWorkspacePage.tsx @@ -20,6 +20,7 @@ import { PeWorkspaceCreateView } from '@/components/pe/PeWorkspaceCreateView' import { api } from '@/lib/api' import { getErrorMessage } from '@/lib/apiError' import { + PurchaseEvaluationPhase, PurchaseEvaluationType, PurchaseEvaluationTypeLabel, type PeDetailBundle, @@ -78,7 +79,9 @@ export function PurchaseEvaluationWorkspacePage() {
- {/* Panel 1: List pure picker + sticky create + pencil edit hover */} + {/* Panel 1: List pure picker + sticky create + pencil edit hover. + Workspace chỉ list phiếu Bản nháp (DangSoanThao) — đã gửi duyệt rồi + không hiện ở đây (vào Danh sách / Duyệt). User 2026-05-07. */} setParams({ mode: 'new', id: null, editHeader: null })} onEditClick={id => setParams({ id, mode: null, editHeader: '1' })} + forcedPhase={PurchaseEvaluationPhase.DangSoanThao} /> {/* Panel 2: Empty | Header form | Detail tabs (workspace mode) */} diff --git a/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx b/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx index f3f42c9..7d348a2 100644 --- a/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx +++ b/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx @@ -14,10 +14,12 @@ import { getErrorMessage } from '@/lib/apiError' import { cn } from '@/lib/cn' import type { Paged } from '@/types/master' import { + PeDisplayStatus, + PeDisplayStatusColor, + PeDisplayStatusLabel, PurchaseEvaluationPhase, - PurchaseEvaluationPhaseColor, - PurchaseEvaluationPhaseLabel, PurchaseEvaluationTypeLabel, + getPeDisplayStatus, type PeDetailBundle, type PeListItem, } from '@/types/purchaseEvaluation' @@ -124,10 +126,19 @@ export function PurchaseEvaluationsListPage() { />
@@ -171,10 +182,10 @@ export function PurchaseEvaluationsListPage() { - {PurchaseEvaluationPhaseLabel[p.phase]} + {PeDisplayStatusLabel[getPeDisplayStatus(p.phase)]}
diff --git a/fe-admin/src/types/purchaseEvaluation.ts b/fe-admin/src/types/purchaseEvaluation.ts index 7cec22c..400ba6a 100644 --- a/fe-admin/src/types/purchaseEvaluation.ts +++ b/fe-admin/src/types/purchaseEvaluation.ts @@ -50,6 +50,42 @@ export const PurchaseEvaluationPhaseColor: Record = { 99: 'bg-red-100 text-red-700', } +// Display status meta — gom các phase chi tiết thành 4 nhóm hiển thị end-user +// friendly. Workflow timeline + workflow service vẫn dùng phase chi tiết. +// User 2026-05-07 chỉnh: +// - Bản nháp = DangSoanThao (chỉ hiện ở Thao tác workspace, ko Duyệt menu) +// - Đã gửi duyệt = bất kỳ phase trung gian (ChoPurchasing/ChoDuAn/ChoCCM/...) +// - Đã duyệt = DaDuyet +// - Từ chối = TuChoi +export const PeDisplayStatus = { + BanNhap: 'BanNhap', + DaGuiDuyet: 'DaGuiDuyet', + DaDuyet: 'DaDuyet', + TuChoi: 'TuChoi', +} as const +export type PeDisplayStatus = typeof PeDisplayStatus[keyof typeof PeDisplayStatus] + +export const PeDisplayStatusLabel: Record = { + BanNhap: 'Bản nháp', + DaGuiDuyet: 'Đã gửi duyệt', + DaDuyet: 'Đã duyệt', + TuChoi: 'Từ chối', +} + +export const PeDisplayStatusColor: Record = { + BanNhap: 'bg-slate-100 text-slate-700', + DaGuiDuyet: 'bg-amber-100 text-amber-700', + DaDuyet: 'bg-emerald-100 text-emerald-700', + TuChoi: 'bg-red-100 text-red-700', +} + +export function getPeDisplayStatus(phase: number): PeDisplayStatus { + if (phase === PurchaseEvaluationPhase.DangSoanThao) return PeDisplayStatus.BanNhap + if (phase === PurchaseEvaluationPhase.DaDuyet) return PeDisplayStatus.DaDuyet + if (phase === PurchaseEvaluationPhase.TuChoi) return PeDisplayStatus.TuChoi + return PeDisplayStatus.DaGuiDuyet +} + export type PeListItem = { id: string maPhieu: string | null diff --git a/fe-user/src/components/pe/PeDetailTabs.tsx b/fe-user/src/components/pe/PeDetailTabs.tsx index c57d238..0ee33ad 100644 --- a/fe-user/src/components/pe/PeDetailTabs.tsx +++ b/fe-user/src/components/pe/PeDetailTabs.tsx @@ -20,10 +20,13 @@ import { PeAttachmentPurposeLabel, PeDepartmentKind, PeDepartmentKindLabel, + PeDisplayStatusColor, + PeDisplayStatusLabel, PurchaseEvaluationPhase, PurchaseEvaluationPhaseColor, PurchaseEvaluationPhaseLabel, PurchaseEvaluationTypeLabel, + getPeDisplayStatus, type PeAttachment, type PeChangelog, type PeDepartmentOpinion, @@ -75,13 +78,18 @@ export function PeDetailTabs({

{evaluation.tenGoiThau}

+ {/* Display status meta (Bản nháp / Đã gửi duyệt / Đã duyệt / Từ chối) + — phase chi tiết hiện ở Workflow timeline Panel 3. */} - {PurchaseEvaluationPhaseLabel[evaluation.phase]} + {PeDisplayStatusLabel[getPeDisplayStatus(evaluation.phase)]} + + + ({PurchaseEvaluationPhaseLabel[evaluation.phase]}) {readOnly && ( diff --git a/fe-user/src/components/pe/PeListPanel.tsx b/fe-user/src/components/pe/PeListPanel.tsx index 89ad018..19a2eb0 100644 --- a/fe-user/src/components/pe/PeListPanel.tsx +++ b/fe-user/src/components/pe/PeListPanel.tsx @@ -16,10 +16,12 @@ import { api } from '@/lib/api' import { cn } from '@/lib/cn' import type { Paged } from '@/types/master' import { + PeDisplayStatus, + PeDisplayStatusColor, + PeDisplayStatusLabel, PurchaseEvaluationPhase, - PurchaseEvaluationPhaseColor, - PurchaseEvaluationPhaseLabel, PurchaseEvaluationTypeLabel, + getPeDisplayStatus, type PeListItem, } from '@/types/purchaseEvaluation' @@ -35,6 +37,7 @@ export function PeListPanel({ showCreateButton = false, onCreate, onEditClick, + forcedPhase, }: { typeFilter: number | null pendingMe?: boolean @@ -48,9 +51,13 @@ export function PeListPanel({ onCreate?: () => void /** Pencil edit icon hover next-to-row — click → select + auto-open Section 1 edit mode (URL ?editHeader=1). */ onEditClick?: (id: string) => void + /** Force phase filter (vd workspace chỉ hiện Bản nháp = DangSoanThao). Khi set: + * ẩn phase Select dropdown + force fetch params phase=. */ + forcedPhase?: number }) { + const effectivePhase = forcedPhase !== undefined ? String(forcedPhase) : phase const list = useQuery({ - queryKey: ['pe-list', { typeFilter, pendingMe, search, phase }], + queryKey: ['pe-list', { typeFilter, pendingMe, search, phase: effectivePhase }], queryFn: async () => { if (pendingMe) { const res = await api.get('/purchase-evaluations/inbox', { @@ -63,7 +70,7 @@ export function PeListPanel({ pageSize: 50, search: search || undefined, type: typeFilter ?? undefined, - phase: phase || undefined, + phase: effectivePhase || undefined, }, }) return res.data @@ -93,12 +100,18 @@ export function PeListPanel({ className="pl-8" />
- + {forcedPhase === undefined ? ( + + ) : ( +
+ Lọc cố định: {PeDisplayStatusLabel[getPeDisplayStatus(forcedPhase)]} +
+ )}
{/* List body */} @@ -150,10 +163,10 @@ export function PeListPanel({ - {PurchaseEvaluationPhaseLabel[p.phase]} + {PeDisplayStatusLabel[getPeDisplayStatus(p.phase)]}
@@ -195,3 +208,16 @@ export function PeListPanel({ ) } + +// Map display status → phase enum cho filter dropdown. +// Bản nháp = DangSoanThao (1), Đã duyệt = DaDuyet (7), Từ chối = TuChoi (99). +// Đã gửi duyệt = không filter exact phase (cần BE hỗ trợ multi-phase filter +// hoặc filter client-side). Tạm thời: trả về '' (không filter) → list show +// hết, user vẫn thấy được phiếu Đã gửi duyệt cùng với tất cả khác. Trade-off +// chấp nhận tới khi BE thêm multi-phase param. +function statusToPhaseValue(status: PeDisplayStatus): string { + if (status === PeDisplayStatus.BanNhap) return String(PurchaseEvaluationPhase.DangSoanThao) + if (status === PeDisplayStatus.DaDuyet) return String(PurchaseEvaluationPhase.DaDuyet) + if (status === PeDisplayStatus.TuChoi) return String(PurchaseEvaluationPhase.TuChoi) + return '' // DaGuiDuyet — multi-phase, không filter exact (TODO BE add support) +} diff --git a/fe-user/src/pages/pe/PurchaseEvaluationWorkspacePage.tsx b/fe-user/src/pages/pe/PurchaseEvaluationWorkspacePage.tsx index 8707212..4870150 100644 --- a/fe-user/src/pages/pe/PurchaseEvaluationWorkspacePage.tsx +++ b/fe-user/src/pages/pe/PurchaseEvaluationWorkspacePage.tsx @@ -20,6 +20,7 @@ import { PeWorkspaceCreateView } from '@/components/pe/PeWorkspaceCreateView' import { api } from '@/lib/api' import { getErrorMessage } from '@/lib/apiError' import { + PurchaseEvaluationPhase, PurchaseEvaluationType, PurchaseEvaluationTypeLabel, type PeDetailBundle, @@ -78,7 +79,9 @@ export function PurchaseEvaluationWorkspacePage() {
- {/* Panel 1: List pure picker + sticky create + pencil edit hover */} + {/* Panel 1: List pure picker + sticky create + pencil edit hover. + Workspace chỉ list phiếu Bản nháp (DangSoanThao) — đã gửi duyệt rồi + không hiện ở đây (vào Danh sách / Duyệt). User 2026-05-07. */} setParams({ mode: 'new', id: null, editHeader: null })} onEditClick={id => setParams({ id, mode: null, editHeader: '1' })} + forcedPhase={PurchaseEvaluationPhase.DangSoanThao} /> {/* Panel 2: Empty | Header form | Detail tabs (workspace mode) */} diff --git a/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx b/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx index f3f42c9..7d348a2 100644 --- a/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx +++ b/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx @@ -14,10 +14,12 @@ import { getErrorMessage } from '@/lib/apiError' import { cn } from '@/lib/cn' import type { Paged } from '@/types/master' import { + PeDisplayStatus, + PeDisplayStatusColor, + PeDisplayStatusLabel, PurchaseEvaluationPhase, - PurchaseEvaluationPhaseColor, - PurchaseEvaluationPhaseLabel, PurchaseEvaluationTypeLabel, + getPeDisplayStatus, type PeDetailBundle, type PeListItem, } from '@/types/purchaseEvaluation' @@ -124,10 +126,19 @@ export function PurchaseEvaluationsListPage() { />
@@ -171,10 +182,10 @@ export function PurchaseEvaluationsListPage() { - {PurchaseEvaluationPhaseLabel[p.phase]} + {PeDisplayStatusLabel[getPeDisplayStatus(p.phase)]}
diff --git a/fe-user/src/types/purchaseEvaluation.ts b/fe-user/src/types/purchaseEvaluation.ts index 7cec22c..400ba6a 100644 --- a/fe-user/src/types/purchaseEvaluation.ts +++ b/fe-user/src/types/purchaseEvaluation.ts @@ -50,6 +50,42 @@ export const PurchaseEvaluationPhaseColor: Record = { 99: 'bg-red-100 text-red-700', } +// Display status meta — gom các phase chi tiết thành 4 nhóm hiển thị end-user +// friendly. Workflow timeline + workflow service vẫn dùng phase chi tiết. +// User 2026-05-07 chỉnh: +// - Bản nháp = DangSoanThao (chỉ hiện ở Thao tác workspace, ko Duyệt menu) +// - Đã gửi duyệt = bất kỳ phase trung gian (ChoPurchasing/ChoDuAn/ChoCCM/...) +// - Đã duyệt = DaDuyet +// - Từ chối = TuChoi +export const PeDisplayStatus = { + BanNhap: 'BanNhap', + DaGuiDuyet: 'DaGuiDuyet', + DaDuyet: 'DaDuyet', + TuChoi: 'TuChoi', +} as const +export type PeDisplayStatus = typeof PeDisplayStatus[keyof typeof PeDisplayStatus] + +export const PeDisplayStatusLabel: Record = { + BanNhap: 'Bản nháp', + DaGuiDuyet: 'Đã gửi duyệt', + DaDuyet: 'Đã duyệt', + TuChoi: 'Từ chối', +} + +export const PeDisplayStatusColor: Record = { + BanNhap: 'bg-slate-100 text-slate-700', + DaGuiDuyet: 'bg-amber-100 text-amber-700', + DaDuyet: 'bg-emerald-100 text-emerald-700', + TuChoi: 'bg-red-100 text-red-700', +} + +export function getPeDisplayStatus(phase: number): PeDisplayStatus { + if (phase === PurchaseEvaluationPhase.DangSoanThao) return PeDisplayStatus.BanNhap + if (phase === PurchaseEvaluationPhase.DaDuyet) return PeDisplayStatus.DaDuyet + if (phase === PurchaseEvaluationPhase.TuChoi) return PeDisplayStatus.TuChoi + return PeDisplayStatus.DaGuiDuyet +} + export type PeListItem = { id: string maPhieu: string | null