[CLAUDE] FE-Admin+FE-User: PE display status meta — Bản nháp / Đã gửi duyệt / Đã duyệt / Từ chối
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Failing after 1m59s
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Failing after 1m59s
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) <noreply@anthropic.com>
This commit is contained in:
@ -20,10 +20,13 @@ import {
|
|||||||
PeAttachmentPurposeLabel,
|
PeAttachmentPurposeLabel,
|
||||||
PeDepartmentKind,
|
PeDepartmentKind,
|
||||||
PeDepartmentKindLabel,
|
PeDepartmentKindLabel,
|
||||||
|
PeDisplayStatusColor,
|
||||||
|
PeDisplayStatusLabel,
|
||||||
PurchaseEvaluationPhase,
|
PurchaseEvaluationPhase,
|
||||||
PurchaseEvaluationPhaseColor,
|
PurchaseEvaluationPhaseColor,
|
||||||
PurchaseEvaluationPhaseLabel,
|
PurchaseEvaluationPhaseLabel,
|
||||||
PurchaseEvaluationTypeLabel,
|
PurchaseEvaluationTypeLabel,
|
||||||
|
getPeDisplayStatus,
|
||||||
type PeAttachment,
|
type PeAttachment,
|
||||||
type PeChangelog,
|
type PeChangelog,
|
||||||
type PeDepartmentOpinion,
|
type PeDepartmentOpinion,
|
||||||
@ -75,13 +78,18 @@ export function PeDetailTabs({
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h2 className="text-base font-semibold text-slate-900">{evaluation.tenGoiThau}</h2>
|
<h2 className="text-base font-semibold text-slate-900">{evaluation.tenGoiThau}</h2>
|
||||||
|
{/* 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. */}
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded px-1.5 py-0.5 text-[11px] font-medium',
|
'rounded px-1.5 py-0.5 text-[11px] font-medium',
|
||||||
PurchaseEvaluationPhaseColor[evaluation.phase],
|
PeDisplayStatusColor[getPeDisplayStatus(evaluation.phase)],
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{PurchaseEvaluationPhaseLabel[evaluation.phase]}
|
{PeDisplayStatusLabel[getPeDisplayStatus(evaluation.phase)]}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-slate-400" title="Phase workflow chi tiết">
|
||||||
|
({PurchaseEvaluationPhaseLabel[evaluation.phase]})
|
||||||
</span>
|
</span>
|
||||||
{readOnly && (
|
{readOnly && (
|
||||||
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-[11px] font-medium text-slate-600">
|
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-[11px] font-medium text-slate-600">
|
||||||
|
|||||||
@ -16,10 +16,12 @@ import { api } from '@/lib/api'
|
|||||||
import { cn } from '@/lib/cn'
|
import { cn } from '@/lib/cn'
|
||||||
import type { Paged } from '@/types/master'
|
import type { Paged } from '@/types/master'
|
||||||
import {
|
import {
|
||||||
|
PeDisplayStatus,
|
||||||
|
PeDisplayStatusColor,
|
||||||
|
PeDisplayStatusLabel,
|
||||||
PurchaseEvaluationPhase,
|
PurchaseEvaluationPhase,
|
||||||
PurchaseEvaluationPhaseColor,
|
|
||||||
PurchaseEvaluationPhaseLabel,
|
|
||||||
PurchaseEvaluationTypeLabel,
|
PurchaseEvaluationTypeLabel,
|
||||||
|
getPeDisplayStatus,
|
||||||
type PeListItem,
|
type PeListItem,
|
||||||
} from '@/types/purchaseEvaluation'
|
} from '@/types/purchaseEvaluation'
|
||||||
|
|
||||||
@ -35,6 +37,7 @@ export function PeListPanel({
|
|||||||
showCreateButton = false,
|
showCreateButton = false,
|
||||||
onCreate,
|
onCreate,
|
||||||
onEditClick,
|
onEditClick,
|
||||||
|
forcedPhase,
|
||||||
}: {
|
}: {
|
||||||
typeFilter: number | null
|
typeFilter: number | null
|
||||||
pendingMe?: boolean
|
pendingMe?: boolean
|
||||||
@ -48,9 +51,13 @@ export function PeListPanel({
|
|||||||
onCreate?: () => void
|
onCreate?: () => void
|
||||||
/** Pencil edit icon hover next-to-row — click → select + auto-open Section 1 edit mode (URL ?editHeader=1). */
|
/** Pencil edit icon hover next-to-row — click → select + auto-open Section 1 edit mode (URL ?editHeader=1). */
|
||||||
onEditClick?: (id: string) => void
|
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=<this>. */
|
||||||
|
forcedPhase?: number
|
||||||
}) {
|
}) {
|
||||||
|
const effectivePhase = forcedPhase !== undefined ? String(forcedPhase) : phase
|
||||||
const list = useQuery({
|
const list = useQuery({
|
||||||
queryKey: ['pe-list', { typeFilter, pendingMe, search, phase }],
|
queryKey: ['pe-list', { typeFilter, pendingMe, search, phase: effectivePhase }],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (pendingMe) {
|
if (pendingMe) {
|
||||||
const res = await api.get<PeListItem[]>('/purchase-evaluations/inbox', {
|
const res = await api.get<PeListItem[]>('/purchase-evaluations/inbox', {
|
||||||
@ -63,7 +70,7 @@ export function PeListPanel({
|
|||||||
pageSize: 50,
|
pageSize: 50,
|
||||||
search: search || undefined,
|
search: search || undefined,
|
||||||
type: typeFilter ?? undefined,
|
type: typeFilter ?? undefined,
|
||||||
phase: phase || undefined,
|
phase: effectivePhase || undefined,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return res.data
|
return res.data
|
||||||
@ -93,12 +100,18 @@ export function PeListPanel({
|
|||||||
className="pl-8"
|
className="pl-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Select value={phase} onChange={e => onPhaseChange(e.target.value)}>
|
{forcedPhase === undefined ? (
|
||||||
<option value="">Tất cả phase</option>
|
<Select value={phase} onChange={e => onPhaseChange(e.target.value)}>
|
||||||
{Object.values(PurchaseEvaluationPhase).map(p => (
|
<option value="">Tất cả trạng thái</option>
|
||||||
<option key={p} value={p}>{PurchaseEvaluationPhaseLabel[p]}</option>
|
{Object.values(PeDisplayStatus).map(s => (
|
||||||
))}
|
<option key={s} value={statusToPhaseValue(s)}>{PeDisplayStatusLabel[s]}</option>
|
||||||
</Select>
|
))}
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<div className="rounded border border-slate-200 bg-slate-50 px-2 py-1.5 text-[11px] text-slate-600">
|
||||||
|
Lọc cố định: <strong>{PeDisplayStatusLabel[getPeDisplayStatus(forcedPhase)]}</strong>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* List body */}
|
{/* List body */}
|
||||||
@ -150,10 +163,10 @@ export function PeListPanel({
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium',
|
'shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium',
|
||||||
PurchaseEvaluationPhaseColor[p.phase],
|
PeDisplayStatusColor[getPeDisplayStatus(p.phase)],
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{PurchaseEvaluationPhaseLabel[p.phase]}
|
{PeDisplayStatusLabel[getPeDisplayStatus(p.phase)]}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 flex items-center justify-between text-[11px] text-slate-500">
|
<div className="mt-1 flex items-center justify-between text-[11px] text-slate-500">
|
||||||
@ -195,3 +208,16 @@ export function PeListPanel({
|
|||||||
</aside>
|
</aside>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import { PeWorkspaceCreateView } from '@/components/pe/PeWorkspaceCreateView'
|
|||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import { getErrorMessage } from '@/lib/apiError'
|
import { getErrorMessage } from '@/lib/apiError'
|
||||||
import {
|
import {
|
||||||
|
PurchaseEvaluationPhase,
|
||||||
PurchaseEvaluationType,
|
PurchaseEvaluationType,
|
||||||
PurchaseEvaluationTypeLabel,
|
PurchaseEvaluationTypeLabel,
|
||||||
type PeDetailBundle,
|
type PeDetailBundle,
|
||||||
@ -78,7 +79,9 @@ export function PurchaseEvaluationWorkspacePage() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[320px_1fr]">
|
<div className="grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[320px_1fr]">
|
||||||
{/* 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. */}
|
||||||
<PeListPanel
|
<PeListPanel
|
||||||
typeFilter={typeFilter}
|
typeFilter={typeFilter}
|
||||||
selectedId={selectedId}
|
selectedId={selectedId}
|
||||||
@ -90,6 +93,7 @@ export function PurchaseEvaluationWorkspacePage() {
|
|||||||
showCreateButton
|
showCreateButton
|
||||||
onCreate={() => setParams({ mode: 'new', id: null, editHeader: null })}
|
onCreate={() => setParams({ mode: 'new', id: null, editHeader: null })}
|
||||||
onEditClick={id => setParams({ id, mode: null, editHeader: '1' })}
|
onEditClick={id => setParams({ id, mode: null, editHeader: '1' })}
|
||||||
|
forcedPhase={PurchaseEvaluationPhase.DangSoanThao}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Panel 2: Empty | Header form | Detail tabs (workspace mode) */}
|
{/* Panel 2: Empty | Header form | Detail tabs (workspace mode) */}
|
||||||
|
|||||||
@ -14,10 +14,12 @@ import { getErrorMessage } from '@/lib/apiError'
|
|||||||
import { cn } from '@/lib/cn'
|
import { cn } from '@/lib/cn'
|
||||||
import type { Paged } from '@/types/master'
|
import type { Paged } from '@/types/master'
|
||||||
import {
|
import {
|
||||||
|
PeDisplayStatus,
|
||||||
|
PeDisplayStatusColor,
|
||||||
|
PeDisplayStatusLabel,
|
||||||
PurchaseEvaluationPhase,
|
PurchaseEvaluationPhase,
|
||||||
PurchaseEvaluationPhaseColor,
|
|
||||||
PurchaseEvaluationPhaseLabel,
|
|
||||||
PurchaseEvaluationTypeLabel,
|
PurchaseEvaluationTypeLabel,
|
||||||
|
getPeDisplayStatus,
|
||||||
type PeDetailBundle,
|
type PeDetailBundle,
|
||||||
type PeListItem,
|
type PeListItem,
|
||||||
} from '@/types/purchaseEvaluation'
|
} from '@/types/purchaseEvaluation'
|
||||||
@ -124,10 +126,19 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Select value={phase} onChange={e => setParam('phase', e.target.value)}>
|
<Select value={phase} onChange={e => setParam('phase', e.target.value)}>
|
||||||
<option value="">Tất cả phase</option>
|
<option value="">Tất cả trạng thái</option>
|
||||||
{Object.values(PurchaseEvaluationPhase).map(p => (
|
{Object.values(PeDisplayStatus).map(s => {
|
||||||
<option key={p} value={p}>{PurchaseEvaluationPhaseLabel[p]}</option>
|
const phaseValue = s === PeDisplayStatus.BanNhap
|
||||||
))}
|
? String(PurchaseEvaluationPhase.DangSoanThao)
|
||||||
|
: s === PeDisplayStatus.DaDuyet
|
||||||
|
? String(PurchaseEvaluationPhase.DaDuyet)
|
||||||
|
: s === PeDisplayStatus.TuChoi
|
||||||
|
? String(PurchaseEvaluationPhase.TuChoi)
|
||||||
|
: '' // DaGuiDuyet — multi-phase, không filter exact (TODO BE)
|
||||||
|
return phaseValue ? (
|
||||||
|
<option key={s} value={phaseValue}>{PeDisplayStatusLabel[s]}</option>
|
||||||
|
) : null
|
||||||
|
})}
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -171,10 +182,10 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium',
|
'shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium',
|
||||||
PurchaseEvaluationPhaseColor[p.phase],
|
PeDisplayStatusColor[getPeDisplayStatus(p.phase)],
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{PurchaseEvaluationPhaseLabel[p.phase]}
|
{PeDisplayStatusLabel[getPeDisplayStatus(p.phase)]}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 flex items-center justify-between text-[11px] text-slate-500">
|
<div className="mt-1 flex items-center justify-between text-[11px] text-slate-500">
|
||||||
|
|||||||
@ -50,6 +50,42 @@ export const PurchaseEvaluationPhaseColor: Record<number, string> = {
|
|||||||
99: 'bg-red-100 text-red-700',
|
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<PeDisplayStatus, string> = {
|
||||||
|
BanNhap: 'Bản nháp',
|
||||||
|
DaGuiDuyet: 'Đã gửi duyệt',
|
||||||
|
DaDuyet: 'Đã duyệt',
|
||||||
|
TuChoi: 'Từ chối',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PeDisplayStatusColor: Record<PeDisplayStatus, string> = {
|
||||||
|
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 = {
|
export type PeListItem = {
|
||||||
id: string
|
id: string
|
||||||
maPhieu: string | null
|
maPhieu: string | null
|
||||||
|
|||||||
@ -20,10 +20,13 @@ import {
|
|||||||
PeAttachmentPurposeLabel,
|
PeAttachmentPurposeLabel,
|
||||||
PeDepartmentKind,
|
PeDepartmentKind,
|
||||||
PeDepartmentKindLabel,
|
PeDepartmentKindLabel,
|
||||||
|
PeDisplayStatusColor,
|
||||||
|
PeDisplayStatusLabel,
|
||||||
PurchaseEvaluationPhase,
|
PurchaseEvaluationPhase,
|
||||||
PurchaseEvaluationPhaseColor,
|
PurchaseEvaluationPhaseColor,
|
||||||
PurchaseEvaluationPhaseLabel,
|
PurchaseEvaluationPhaseLabel,
|
||||||
PurchaseEvaluationTypeLabel,
|
PurchaseEvaluationTypeLabel,
|
||||||
|
getPeDisplayStatus,
|
||||||
type PeAttachment,
|
type PeAttachment,
|
||||||
type PeChangelog,
|
type PeChangelog,
|
||||||
type PeDepartmentOpinion,
|
type PeDepartmentOpinion,
|
||||||
@ -75,13 +78,18 @@ export function PeDetailTabs({
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h2 className="text-base font-semibold text-slate-900">{evaluation.tenGoiThau}</h2>
|
<h2 className="text-base font-semibold text-slate-900">{evaluation.tenGoiThau}</h2>
|
||||||
|
{/* 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. */}
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded px-1.5 py-0.5 text-[11px] font-medium',
|
'rounded px-1.5 py-0.5 text-[11px] font-medium',
|
||||||
PurchaseEvaluationPhaseColor[evaluation.phase],
|
PeDisplayStatusColor[getPeDisplayStatus(evaluation.phase)],
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{PurchaseEvaluationPhaseLabel[evaluation.phase]}
|
{PeDisplayStatusLabel[getPeDisplayStatus(evaluation.phase)]}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-slate-400" title="Phase workflow chi tiết">
|
||||||
|
({PurchaseEvaluationPhaseLabel[evaluation.phase]})
|
||||||
</span>
|
</span>
|
||||||
{readOnly && (
|
{readOnly && (
|
||||||
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-[11px] font-medium text-slate-600">
|
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-[11px] font-medium text-slate-600">
|
||||||
|
|||||||
@ -16,10 +16,12 @@ import { api } from '@/lib/api'
|
|||||||
import { cn } from '@/lib/cn'
|
import { cn } from '@/lib/cn'
|
||||||
import type { Paged } from '@/types/master'
|
import type { Paged } from '@/types/master'
|
||||||
import {
|
import {
|
||||||
|
PeDisplayStatus,
|
||||||
|
PeDisplayStatusColor,
|
||||||
|
PeDisplayStatusLabel,
|
||||||
PurchaseEvaluationPhase,
|
PurchaseEvaluationPhase,
|
||||||
PurchaseEvaluationPhaseColor,
|
|
||||||
PurchaseEvaluationPhaseLabel,
|
|
||||||
PurchaseEvaluationTypeLabel,
|
PurchaseEvaluationTypeLabel,
|
||||||
|
getPeDisplayStatus,
|
||||||
type PeListItem,
|
type PeListItem,
|
||||||
} from '@/types/purchaseEvaluation'
|
} from '@/types/purchaseEvaluation'
|
||||||
|
|
||||||
@ -35,6 +37,7 @@ export function PeListPanel({
|
|||||||
showCreateButton = false,
|
showCreateButton = false,
|
||||||
onCreate,
|
onCreate,
|
||||||
onEditClick,
|
onEditClick,
|
||||||
|
forcedPhase,
|
||||||
}: {
|
}: {
|
||||||
typeFilter: number | null
|
typeFilter: number | null
|
||||||
pendingMe?: boolean
|
pendingMe?: boolean
|
||||||
@ -48,9 +51,13 @@ export function PeListPanel({
|
|||||||
onCreate?: () => void
|
onCreate?: () => void
|
||||||
/** Pencil edit icon hover next-to-row — click → select + auto-open Section 1 edit mode (URL ?editHeader=1). */
|
/** Pencil edit icon hover next-to-row — click → select + auto-open Section 1 edit mode (URL ?editHeader=1). */
|
||||||
onEditClick?: (id: string) => void
|
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=<this>. */
|
||||||
|
forcedPhase?: number
|
||||||
}) {
|
}) {
|
||||||
|
const effectivePhase = forcedPhase !== undefined ? String(forcedPhase) : phase
|
||||||
const list = useQuery({
|
const list = useQuery({
|
||||||
queryKey: ['pe-list', { typeFilter, pendingMe, search, phase }],
|
queryKey: ['pe-list', { typeFilter, pendingMe, search, phase: effectivePhase }],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (pendingMe) {
|
if (pendingMe) {
|
||||||
const res = await api.get<PeListItem[]>('/purchase-evaluations/inbox', {
|
const res = await api.get<PeListItem[]>('/purchase-evaluations/inbox', {
|
||||||
@ -63,7 +70,7 @@ export function PeListPanel({
|
|||||||
pageSize: 50,
|
pageSize: 50,
|
||||||
search: search || undefined,
|
search: search || undefined,
|
||||||
type: typeFilter ?? undefined,
|
type: typeFilter ?? undefined,
|
||||||
phase: phase || undefined,
|
phase: effectivePhase || undefined,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return res.data
|
return res.data
|
||||||
@ -93,12 +100,18 @@ export function PeListPanel({
|
|||||||
className="pl-8"
|
className="pl-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Select value={phase} onChange={e => onPhaseChange(e.target.value)}>
|
{forcedPhase === undefined ? (
|
||||||
<option value="">Tất cả phase</option>
|
<Select value={phase} onChange={e => onPhaseChange(e.target.value)}>
|
||||||
{Object.values(PurchaseEvaluationPhase).map(p => (
|
<option value="">Tất cả trạng thái</option>
|
||||||
<option key={p} value={p}>{PurchaseEvaluationPhaseLabel[p]}</option>
|
{Object.values(PeDisplayStatus).map(s => (
|
||||||
))}
|
<option key={s} value={statusToPhaseValue(s)}>{PeDisplayStatusLabel[s]}</option>
|
||||||
</Select>
|
))}
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<div className="rounded border border-slate-200 bg-slate-50 px-2 py-1.5 text-[11px] text-slate-600">
|
||||||
|
Lọc cố định: <strong>{PeDisplayStatusLabel[getPeDisplayStatus(forcedPhase)]}</strong>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* List body */}
|
{/* List body */}
|
||||||
@ -150,10 +163,10 @@ export function PeListPanel({
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium',
|
'shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium',
|
||||||
PurchaseEvaluationPhaseColor[p.phase],
|
PeDisplayStatusColor[getPeDisplayStatus(p.phase)],
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{PurchaseEvaluationPhaseLabel[p.phase]}
|
{PeDisplayStatusLabel[getPeDisplayStatus(p.phase)]}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 flex items-center justify-between text-[11px] text-slate-500">
|
<div className="mt-1 flex items-center justify-between text-[11px] text-slate-500">
|
||||||
@ -195,3 +208,16 @@ export function PeListPanel({
|
|||||||
</aside>
|
</aside>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import { PeWorkspaceCreateView } from '@/components/pe/PeWorkspaceCreateView'
|
|||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import { getErrorMessage } from '@/lib/apiError'
|
import { getErrorMessage } from '@/lib/apiError'
|
||||||
import {
|
import {
|
||||||
|
PurchaseEvaluationPhase,
|
||||||
PurchaseEvaluationType,
|
PurchaseEvaluationType,
|
||||||
PurchaseEvaluationTypeLabel,
|
PurchaseEvaluationTypeLabel,
|
||||||
type PeDetailBundle,
|
type PeDetailBundle,
|
||||||
@ -78,7 +79,9 @@ export function PurchaseEvaluationWorkspacePage() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[320px_1fr]">
|
<div className="grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[320px_1fr]">
|
||||||
{/* 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. */}
|
||||||
<PeListPanel
|
<PeListPanel
|
||||||
typeFilter={typeFilter}
|
typeFilter={typeFilter}
|
||||||
selectedId={selectedId}
|
selectedId={selectedId}
|
||||||
@ -90,6 +93,7 @@ export function PurchaseEvaluationWorkspacePage() {
|
|||||||
showCreateButton
|
showCreateButton
|
||||||
onCreate={() => setParams({ mode: 'new', id: null, editHeader: null })}
|
onCreate={() => setParams({ mode: 'new', id: null, editHeader: null })}
|
||||||
onEditClick={id => setParams({ id, mode: null, editHeader: '1' })}
|
onEditClick={id => setParams({ id, mode: null, editHeader: '1' })}
|
||||||
|
forcedPhase={PurchaseEvaluationPhase.DangSoanThao}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Panel 2: Empty | Header form | Detail tabs (workspace mode) */}
|
{/* Panel 2: Empty | Header form | Detail tabs (workspace mode) */}
|
||||||
|
|||||||
@ -14,10 +14,12 @@ import { getErrorMessage } from '@/lib/apiError'
|
|||||||
import { cn } from '@/lib/cn'
|
import { cn } from '@/lib/cn'
|
||||||
import type { Paged } from '@/types/master'
|
import type { Paged } from '@/types/master'
|
||||||
import {
|
import {
|
||||||
|
PeDisplayStatus,
|
||||||
|
PeDisplayStatusColor,
|
||||||
|
PeDisplayStatusLabel,
|
||||||
PurchaseEvaluationPhase,
|
PurchaseEvaluationPhase,
|
||||||
PurchaseEvaluationPhaseColor,
|
|
||||||
PurchaseEvaluationPhaseLabel,
|
|
||||||
PurchaseEvaluationTypeLabel,
|
PurchaseEvaluationTypeLabel,
|
||||||
|
getPeDisplayStatus,
|
||||||
type PeDetailBundle,
|
type PeDetailBundle,
|
||||||
type PeListItem,
|
type PeListItem,
|
||||||
} from '@/types/purchaseEvaluation'
|
} from '@/types/purchaseEvaluation'
|
||||||
@ -124,10 +126,19 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Select value={phase} onChange={e => setParam('phase', e.target.value)}>
|
<Select value={phase} onChange={e => setParam('phase', e.target.value)}>
|
||||||
<option value="">Tất cả phase</option>
|
<option value="">Tất cả trạng thái</option>
|
||||||
{Object.values(PurchaseEvaluationPhase).map(p => (
|
{Object.values(PeDisplayStatus).map(s => {
|
||||||
<option key={p} value={p}>{PurchaseEvaluationPhaseLabel[p]}</option>
|
const phaseValue = s === PeDisplayStatus.BanNhap
|
||||||
))}
|
? String(PurchaseEvaluationPhase.DangSoanThao)
|
||||||
|
: s === PeDisplayStatus.DaDuyet
|
||||||
|
? String(PurchaseEvaluationPhase.DaDuyet)
|
||||||
|
: s === PeDisplayStatus.TuChoi
|
||||||
|
? String(PurchaseEvaluationPhase.TuChoi)
|
||||||
|
: '' // DaGuiDuyet — multi-phase, không filter exact (TODO BE)
|
||||||
|
return phaseValue ? (
|
||||||
|
<option key={s} value={phaseValue}>{PeDisplayStatusLabel[s]}</option>
|
||||||
|
) : null
|
||||||
|
})}
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -171,10 +182,10 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium',
|
'shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium',
|
||||||
PurchaseEvaluationPhaseColor[p.phase],
|
PeDisplayStatusColor[getPeDisplayStatus(p.phase)],
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{PurchaseEvaluationPhaseLabel[p.phase]}
|
{PeDisplayStatusLabel[getPeDisplayStatus(p.phase)]}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 flex items-center justify-between text-[11px] text-slate-500">
|
<div className="mt-1 flex items-center justify-between text-[11px] text-slate-500">
|
||||||
|
|||||||
@ -50,6 +50,42 @@ export const PurchaseEvaluationPhaseColor: Record<number, string> = {
|
|||||||
99: 'bg-red-100 text-red-700',
|
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<PeDisplayStatus, string> = {
|
||||||
|
BanNhap: 'Bản nháp',
|
||||||
|
DaGuiDuyet: 'Đã gửi duyệt',
|
||||||
|
DaDuyet: 'Đã duyệt',
|
||||||
|
TuChoi: 'Từ chối',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PeDisplayStatusColor: Record<PeDisplayStatus, string> = {
|
||||||
|
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 = {
|
export type PeListItem = {
|
||||||
id: string
|
id: string
|
||||||
maPhieu: string | null
|
maPhieu: string | null
|
||||||
|
|||||||
Reference in New Issue
Block a user