+ {editId
+ ? 'Chỉ sửa các field thông tin chung — NCC + báo giá + ý kiến nhập ở Panel chi tiết.'
+ : 'Tạo header trước, sau đó nhập NCC + Báo giá + Hạng mục ở Panel chi tiết.'}
+
+
+
+
+
+
+
+
+
+
+ setForm({ ...form, tenGoiThau: e.target.value })}
+ placeholder="vd Cung cấp bê tông"
+ />
+
+
+
+
+
+
+
+
+
+
+
+ {!form.projectId
+ ? 'Chọn dự án trước để xem ngân sách khả dụng.'
+ : eligibleBudgets.data && eligibleBudgets.data.length === 0
+ ? 'Dự án này chưa có ngân sách đã duyệt.'
+ : 'Chỉ list ngân sách đã duyệt cùng dự án.'}
+
+
+
+
+
+ setForm({ ...form, diaDiem: e.target.value })}
+ placeholder="Lô K, KCN Lộc An - Bình Sơn..."
+ />
+
+
+
+
+
+
+
+
+
+
+
+ {onCancel && (
+
+ )}
+
+
+
+ )
+}
diff --git a/fe-user/src/components/pe/PeListPanel.tsx b/fe-user/src/components/pe/PeListPanel.tsx
new file mode 100644
index 0000000..9321f82
--- /dev/null
+++ b/fe-user/src/components/pe/PeListPanel.tsx
@@ -0,0 +1,184 @@
+// Pure picker panel cho workspace 2-panel "Thao tác" (Pe_*_Create leaf).
+// KHÔNG có inline Edit/Delete (per Q1 user 2026-05-07): chỉ click để pick, +
+// optional sticky bottom "+ Thêm mới" button khi showCreateButton=true.
+//
+// Reuse-able: caller quản URL state qua props (search/phase/typeFilter), panel
+// chỉ render + invoke callbacks. Pendingme vẫn truyền được nếu cần dùng cho
+// inbox view khác (hiện chỉ workspace dùng pendingMe=false).
+import { useQuery } from '@tanstack/react-query'
+import { ClipboardCheck, Plus, Search } from 'lucide-react'
+import { Button } from '@/components/ui/Button'
+import { Input } from '@/components/ui/Input'
+import { Select } from '@/components/ui/Select'
+import { EmptyState } from '@/components/EmptyState'
+import { SlaTimer } from '@/components/SlaTimer'
+import { api } from '@/lib/api'
+import { cn } from '@/lib/cn'
+import type { Paged } from '@/types/master'
+import {
+ PurchaseEvaluationPhase,
+ PurchaseEvaluationPhaseColor,
+ PurchaseEvaluationPhaseLabel,
+ PurchaseEvaluationTypeLabel,
+ type PeListItem,
+} from '@/types/purchaseEvaluation'
+
+export function PeListPanel({
+ typeFilter,
+ pendingMe = false,
+ selectedId,
+ search,
+ phase,
+ onSelect,
+ onSearchChange,
+ onPhaseChange,
+ showCreateButton = false,
+ onCreate,
+}: {
+ typeFilter: number | null
+ pendingMe?: boolean
+ selectedId: string | null
+ search: string
+ phase: string
+ onSelect: (id: string) => void
+ onSearchChange: (q: string) => void
+ onPhaseChange: (p: string) => void
+ showCreateButton?: boolean
+ onCreate?: () => void
+}) {
+ const list = useQuery({
+ queryKey: ['pe-list', { typeFilter, pendingMe, search, phase }],
+ queryFn: async () => {
+ if (pendingMe) {
+ const res = await api.get('/purchase-evaluations/inbox', {
+ params: { type: typeFilter ?? undefined },
+ })
+ return { items: res.data, total: res.data.length, page: 1, pageSize: res.data.length }
+ }
+ const res = await api.get>('/purchase-evaluations', {
+ params: {
+ pageSize: 50,
+ search: search || undefined,
+ type: typeFilter ?? undefined,
+ phase: phase || undefined,
+ },
+ })
+ return res.data
+ },
+ })
+
+ const rows = list.data?.items ?? []
+
+ return (
+
+ )
+}
diff --git a/fe-user/src/pages/pe/PurchaseEvaluationWorkspacePage.tsx b/fe-user/src/pages/pe/PurchaseEvaluationWorkspacePage.tsx
new file mode 100644
index 0000000..c7eddb6
--- /dev/null
+++ b/fe-user/src/pages/pe/PurchaseEvaluationWorkspacePage.tsx
@@ -0,0 +1,140 @@
+// Workspace 2-panel cho leaf "Thao tác" Pe_*_Create (Type A=DuyetNcc / B=
+// DuyetNccPhuongAn). Pattern mirror HĐ Thầu phụ ContractCreatePage:
+// Panel 1 (320px): list pure picker (read-only, không edit/delete) + sticky
+// "+ Thêm mới" bottom button (Q1 user 2026-05-07).
+// Panel 2 (1fr): empty state · mode=new · else
+// (5 section + Section 5
+// Ý kiến 4PB DISABLED — Q5: nhập ở leaf "Duyệt").
+//
+// URL: /purchase-evaluations/workspace?type={1|2}[&id=...][&mode=new][&q=][&phase=]
+// Workflow Panel + Approvals + History KHÔNG render ở workspace (Q1 — chỉ
+// hiện ở leaf Danh sách + Duyệt vẫn 3-panel).
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
+import { useNavigate, useSearchParams } from 'react-router-dom'
+import { toast } from 'sonner'
+import { ClipboardCheck } from 'lucide-react'
+import { EmptyState } from '@/components/EmptyState'
+import { PeDetailTabs } from '@/components/pe/PeDetailTabs'
+import { PeListPanel } from '@/components/pe/PeListPanel'
+import { PeHeaderForm } from '@/components/pe/PeHeaderForm'
+import { api } from '@/lib/api'
+import { getErrorMessage } from '@/lib/apiError'
+import {
+ PurchaseEvaluationType,
+ PurchaseEvaluationTypeLabel,
+ type PeDetailBundle,
+} from '@/types/purchaseEvaluation'
+
+export function PurchaseEvaluationWorkspacePage() {
+ const navigate = useNavigate()
+ const qc = useQueryClient()
+ const [sp, setSp] = useSearchParams()
+ const typeFilter = sp.get('type') ? Number(sp.get('type')) : PurchaseEvaluationType.DuyetNcc
+ const search = sp.get('q') ?? ''
+ const phase = sp.get('phase') ?? ''
+ const selectedId = sp.get('id')
+ const mode = sp.get('mode') // 'new' | null
+
+ const detail = useQuery({
+ queryKey: ['pe-detail', selectedId],
+ queryFn: async () => (await api.get(`/purchase-evaluations/${selectedId}`)).data,
+ enabled: !!selectedId,
+ })
+
+ const del = useMutation({
+ mutationFn: async (id: string) => api.delete(`/purchase-evaluations/${id}`),
+ onSuccess: () => {
+ toast.success('Đã xóa phiếu.')
+ setParams({ id: null })
+ qc.invalidateQueries({ queryKey: ['pe-list'] })
+ },
+ onError: e => toast.error(getErrorMessage(e)),
+ })
+
+ function setParams(updates: Record) {
+ const next = new URLSearchParams(sp)
+ for (const [k, v] of Object.entries(updates)) {
+ if (v == null || v === '') next.delete(k)
+ else next.set(k, v)
+ }
+ // Search input gõ liên tục → replace (không spam history); pick/mode → push
+ const replace = Object.keys(updates).length === 1 && updates.q !== undefined
+ setSp(next, { replace })
+ }
+
+ const headerTitle = `${PurchaseEvaluationTypeLabel[typeFilter]} — Thao tác`
+
+ return (
+
+
+
+
+
{headerTitle}
+
+
+ Workspace 2-panel — Workflow + Duyệt ở menu “Duyệt”.
+