[CLAUDE] FE-Admin: PE Thao tác 2-panel workspace + Panel 1 read-only picker + Section 5 disabled

Chunk 1/3 — restructure leaf "Thao tác" (Pe_*_Create) từ page tạo header riêng
sang workspace 2-panel mirror pattern HĐ Thầu phụ ContractCreatePage:
  Panel 1 (320px): list pure picker (KHÔNG inline edit/delete per Q1 user) +
                   sticky "+ Thêm mới" bottom button.
  Panel 2 (1fr):   empty state | mode=new <PeHeaderForm> | <PeDetailTabs
                   mode="workspace"> (5 section, Section 5 Ý kiến 4PB DISABLED
                   per Q5 user — nhập ở leaf "Duyệt").

Workflow Panel + Approvals + History KHÔNG render trong workspace (Q1) — chỉ
hiện ở leaf "Danh sách" + "Duyệt" giữ nguyên 3-panel hiện tại (Q3).

URL: /purchase-evaluations/workspace?type={1|2}[&id=...][&mode=new][&q=][&phase=]
Menu resolver Pe_*_Create: /purchase-evaluations/new?type=N → /workspace?type=N.
Route mới /workspace; route /new giữ tồn tại cho deep-link "Sửa header" button.

Files:
  + fe-admin/src/components/pe/PeListPanel.tsx (~180 LOC) — pure picker reuseable
  + fe-admin/src/components/pe/PeHeaderForm.tsx (~210 LOC) — extract header form
  + fe-admin/src/pages/pe/PurchaseEvaluationWorkspacePage.tsx (~120 LOC)
  ~ fe-admin/src/components/pe/PeDetailTabs.tsx — add mode prop + Section 5 hint
  ~ fe-admin/src/components/Layout.tsx — resolver Pe_*_Create map workspace
  ~ fe-admin/src/App.tsx — route /purchase-evaluations/workspace

Verify: npm run build pass · dotnet test 83 vẫn pass (54 Domain + 29 Infra).
fe-user mirror = Chunk 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-07 10:36:06 +07:00
parent b7a153e45a
commit ee0d3608e7
6 changed files with 571 additions and 2 deletions

View File

@ -21,6 +21,7 @@ import { ReportsPage } from '@/pages/ReportsPage'
import { UsersPage } from '@/pages/system/UsersPage'
import { PurchaseEvaluationsListPage, PurchaseEvaluationDetailPage } from '@/pages/pe/PurchaseEvaluationsListPage'
import { PurchaseEvaluationCreatePage } from '@/pages/pe/PurchaseEvaluationCreatePage'
import { PurchaseEvaluationWorkspacePage } from '@/pages/pe/PurchaseEvaluationWorkspacePage'
import { BudgetsListPage, BudgetDetailPage } from '@/pages/budgets/BudgetsListPage'
import { BudgetCreatePage } from '@/pages/budgets/BudgetCreatePage'
@ -55,6 +56,7 @@ function App() {
<Route path="/contracts/new" element={<ContractCreatePage />} />
<Route path="/contracts/:id" element={<ContractDetailPage />} />
<Route path="/purchase-evaluations" element={<PurchaseEvaluationsListPage />} />
<Route path="/purchase-evaluations/workspace" element={<PurchaseEvaluationWorkspacePage />} />
<Route path="/purchase-evaluations/new" element={<PurchaseEvaluationCreatePage />} />
<Route path="/purchase-evaluations/:id" element={<PurchaseEvaluationDetailPage />} />
<Route path="/budgets" element={<BudgetsListPage />} />

View File

@ -78,7 +78,10 @@ function resolvePath(key: string): string | null {
const typeInt = PE_CODE_TO_INT[code]
if (!typeInt) return null
if (action === 'List') return `/purchase-evaluations?type=${typeInt}`
if (action === 'Create') return `/purchase-evaluations/new?type=${typeInt}`
// "Thao tác" leaf → workspace 2-panel (Q4 2026-05-07): pick + create + sửa
// tables inline. Header-only `/new` page giữ tồn tại cho deep-link cũ
// (PeDetailTabs "Sửa header" button vẫn navigate sang đó).
if (action === 'Create') return `/purchase-evaluations/workspace?type=${typeInt}`
if (action === 'Pending') return `/purchase-evaluations?type=${typeInt}&pendingMe=1`
}
// PE workflow admin leaf: PeWf_<Code> → /system/pe-workflows/<code>

View File

@ -38,20 +38,32 @@ const fmtMoney = (v: number) => v.toLocaleString('vi-VN')
// Main detail content — flat render 3 section không tabs.
// Tên giữ PeDetailTabs để không break callsite (rename gây churn).
//
// `mode` (2026-05-07):
// - 'detail' (default): full UX — Section 5 Ý kiến 4PB editable theo readOnly.
// Dùng ở leaf "Danh sách" + "Duyệt" (3-panel pages).
// - 'workspace': dùng ở leaf "Thao tác" (2-panel workspace). Section 5 LUÔN
// disabled (Q5 user — ý kiến nhập khi duyệt, không phải workspace nhập liệu).
// Workflow Panel + Approvals + History KHÔNG render trong PeDetailTabs (luôn
// ở caller PeWorkflowPanel — workspace caller skip render Panel 3 hoàn toàn).
export function PeDetailTabs({
evaluation,
onBack,
onDelete,
readOnly = false,
mode = 'detail',
}: {
evaluation: PeDetailBundle
onBack: () => void
onDelete: () => void
/** Menu "Duyệt" (pendingMe=1) — ẩn mọi action thêm/sửa/xóa, chỉ xem + duyệt phase. */
readOnly?: boolean
/** 'workspace' = Section 5 LUÔN disabled (ý kiến nhập ở leaf Duyệt). */
mode?: 'detail' | 'workspace'
}) {
const navigate = useNavigate()
const isDraft = evaluation.phase === PurchaseEvaluationPhase.DangSoanThao
const opinionsReadOnly = readOnly || mode === 'workspace'
return (
<div className="rounded-lg border border-slate-200 bg-white shadow-sm">
@ -112,7 +124,12 @@ export function PeDetailTabs({
<ItemsTab ev={evaluation} readOnly={readOnly} />
</Section>
<Section title="5. Ý kiến 4 phòng ban (sign-off)">
<DepartmentOpinionsSection ev={evaluation} readOnly={readOnly} />
{mode === 'workspace' && (
<div className="mb-3 rounded border border-amber-200 bg-amber-50 px-3 py-2 text-[12px] text-amber-800">
Ý kiến + chữ nhập khi duyệt phiếu vào menu &ldquo;Duyệt&rdquo; đ .
</div>
)}
<DepartmentOpinionsSection ev={evaluation} readOnly={opinionsReadOnly} />
</Section>
</div>
</div>

View File

@ -0,0 +1,223 @@
// Header form cho phiếu Duyệt NCC — tách từ PurchaseEvaluationCreatePage để
// reuse trong Workspace mode "new". Sửa header sau khi tạo vẫn redirect về
// page Create cũ (`/purchase-evaluations/new?id=`) — workspace KHÔNG re-edit
// header.
import { useEffect, useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Label } from '@/components/ui/Label'
import { Select } from '@/components/ui/Select'
import { Textarea } from '@/components/ui/Textarea'
import { api } from '@/lib/api'
import { getErrorMessage } from '@/lib/apiError'
import {
PurchaseEvaluationType,
PurchaseEvaluationTypeLabel,
type PeDetailBundle,
} from '@/types/purchaseEvaluation'
import { BudgetPhase, type BudgetListItem } from '@/types/budget'
import type { Paged, Project } from '@/types/master'
export function PeHeaderForm({
editId,
defaultType,
onSaved,
onCancel,
}: {
editId?: string | null
defaultType?: number
/** Gọi sau khi save thành công với (newId, type). Caller decide navigation. */
onSaved: (id: string, type: number) => void
onCancel?: () => void
}) {
const qc = useQueryClient()
const initialType = defaultType ?? PurchaseEvaluationType.DuyetNcc
const projects = useQuery({
queryKey: ['all-projects'],
queryFn: async () => (await api.get<{ items: Project[] }>('/projects', { params: { pageSize: 1000 } })).data.items,
})
const existing = useQuery({
queryKey: ['pe-detail', editId],
queryFn: async () => (await api.get<PeDetailBundle>(`/purchase-evaluations/${editId}`)).data,
enabled: !!editId,
})
const [form, setForm] = useState({
type: initialType as number,
tenGoiThau: '',
projectId: '',
diaDiem: '',
moTa: '',
paymentTerms: '',
budgetId: '' as string,
})
const eligibleBudgets = useQuery({
queryKey: ['eligible-budgets', form.projectId],
queryFn: async () => {
const res = await api.get<Paged<BudgetListItem>>('/budgets', {
params: { pageSize: 100, projectId: form.projectId, phase: BudgetPhase.DaDuyet },
})
return res.data.items
},
enabled: !!form.projectId,
})
useEffect(() => {
if (existing.data) {
setForm({
type: existing.data.type,
tenGoiThau: existing.data.tenGoiThau,
projectId: existing.data.projectId,
diaDiem: existing.data.diaDiem ?? '',
moTa: existing.data.moTa ?? '',
paymentTerms: existing.data.paymentTerms ?? '',
budgetId: existing.data.budgetId ?? '',
})
}
}, [existing.data])
const mut = useMutation({
mutationFn: async () => {
if (editId) {
return api.put(`/purchase-evaluations/${editId}`, {
id: editId,
tenGoiThau: form.tenGoiThau,
diaDiem: form.diaDiem || null,
moTa: form.moTa || null,
paymentTerms: form.paymentTerms || null,
budgetId: form.budgetId || null,
})
}
return api.post<{ id: string }>('/purchase-evaluations', {
type: form.type,
tenGoiThau: form.tenGoiThau,
projectId: form.projectId,
diaDiem: form.diaDiem || null,
moTa: form.moTa || null,
paymentTerms: form.paymentTerms || null,
budgetId: form.budgetId || null,
})
},
onSuccess: res => {
toast.success(editId ? 'Đã lưu.' : 'Đã tạo phiếu.')
qc.invalidateQueries({ queryKey: ['pe-list'] })
const id = editId ?? (res as { data: { id: string } }).data.id
onSaved(id, form.type)
},
onError: e => toast.error(getErrorMessage(e)),
})
return (
<div className="max-w-2xl space-y-4 rounded-lg border border-slate-200 bg-white p-6 shadow-sm">
<header>
<h2 className="text-base font-semibold tracking-tight text-slate-900">
{editId ? 'Sửa header phiếu' : 'Tạo phiếu Duyệt NCC mới'}
</h2>
<p className="mt-0.5 text-[12px] text-slate-500">
{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.'}
</p>
</header>
<div>
<Label>Loại quy trình</Label>
<Select
value={form.type}
disabled={!!editId}
onChange={e => setForm({ ...form, type: Number(e.target.value) })}
>
{Object.values(PurchaseEvaluationType).map(t => (
<option key={t} value={t}>{PurchaseEvaluationTypeLabel[t]}</option>
))}
</Select>
</div>
<div>
<Label>Tên gói thầu *</Label>
<Input
value={form.tenGoiThau}
onChange={e => setForm({ ...form, tenGoiThau: e.target.value })}
placeholder="vd Cung cấp bê tông"
/>
</div>
<div>
<Label>Dự án *</Label>
<Select
value={form.projectId}
disabled={!!editId}
onChange={e => setForm({ ...form, projectId: e.target.value })}
>
<option value="">-- Chọn --</option>
{projects.data?.map(p => (
<option key={p.id} value={p.id}>{p.code} {p.name}</option>
))}
</Select>
</div>
<div>
<Label>Ngân sách (đi chiếu chi phí)</Label>
<Select
value={form.budgetId}
disabled={!form.projectId}
onChange={e => setForm({ ...form, budgetId: e.target.value })}
>
<option value=""> (không link)</option>
{eligibleBudgets.data?.map(b => (
<option key={b.id} value={b.id}>
{b.maNganSach ?? '—'} · {b.tenNganSach} · {b.tongNganSach.toLocaleString('vi-VN')} đ
</option>
))}
</Select>
<p className="mt-1 text-[11px] text-slate-500">
{!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.'}
</p>
</div>
<div>
<Label>Đa điểm</Label>
<Input
value={form.diaDiem}
onChange={e => setForm({ ...form, diaDiem: e.target.value })}
placeholder="Lô K, KCN Lộc An - Bình Sơn..."
/>
</div>
<div>
<Label> tả</Label>
<Textarea rows={3} value={form.moTa} onChange={e => setForm({ ...form, moTa: e.target.value })} />
</div>
<div>
<Label>Điều khoản thanh toán (JSON hoặc text)</Label>
<Textarea
rows={3}
value={form.paymentTerms}
onChange={e => setForm({ ...form, paymentTerms: e.target.value })}
placeholder='{"tamUng":"10%","thanhToanTam":"100% W.done","quyetToan":"Final Account","baoHanh":"5%"}'
/>
</div>
<div className="flex justify-end gap-2">
{onCancel && (
<Button variant="ghost" onClick={onCancel}>Hủy</Button>
)}
<Button
onClick={() => mut.mutate()}
disabled={!form.tenGoiThau || !form.projectId || mut.isPending}
>
{editId ? 'Lưu' : 'Tạo phiếu'}
</Button>
</div>
</div>
)
}

View File

@ -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<PeListItem[]>('/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<Paged<PeListItem>>('/purchase-evaluations', {
params: {
pageSize: 50,
search: search || undefined,
type: typeFilter ?? undefined,
phase: phase || undefined,
},
})
return res.data
},
})
const rows = list.data?.items ?? []
return (
<aside className="flex flex-col overflow-hidden border-r border-slate-200 bg-white">
{/* Header — count + filter */}
<div className="space-y-2 border-b border-slate-200 p-3">
<div className="flex items-center justify-between">
<div className="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
Danh sách phiếu
</div>
<span className="rounded-full bg-slate-100 px-2 py-0.5 text-[11px] font-medium text-slate-600">
{list.data?.total ?? 0}
</span>
</div>
<div className="relative">
<Search className="pointer-events-none absolute left-2.5 top-2.5 h-4 w-4 text-slate-400" />
<Input
value={search}
onChange={e => onSearchChange(e.target.value)}
placeholder="Tìm mã / tên gói thầu / dự án…"
className="pl-8"
/>
</div>
<Select value={phase} onChange={e => onPhaseChange(e.target.value)}>
<option value="">Tất cả phase</option>
{Object.values(PurchaseEvaluationPhase).map(p => (
<option key={p} value={p}>{PurchaseEvaluationPhaseLabel[p]}</option>
))}
</Select>
</div>
{/* List body */}
<div className="flex-1 overflow-y-auto">
{list.isLoading && (
<div className="space-y-2 p-3">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="h-16 animate-pulse rounded-md bg-slate-100" />
))}
</div>
)}
{!list.isLoading && rows.length === 0 && (
<div className="p-6">
<EmptyState
icon={ClipboardCheck}
title="Chưa có phiếu"
description={
showCreateButton
? 'Bấm + Thêm mới ở dưới để tạo phiếu đầu tiên.'
: 'Chưa có phiếu nào khớp bộ lọc.'
}
/>
</div>
)}
<ul className="divide-y divide-slate-100">
{rows.map(p => (
<li key={p.id}>
<button
onClick={() => onSelect(p.id)}
className={cn(
'block w-full px-3 py-2.5 text-left transition hover:bg-slate-50',
selectedId === p.id && 'bg-brand-50 ring-1 ring-inset ring-brand-200',
)}
>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<div className="truncate text-[13px] font-medium text-slate-900">{p.tenGoiThau}</div>
<div className="mt-0.5 flex items-center gap-1.5 text-[11px] text-slate-500">
<span className="font-mono">{p.maPhieu ?? '—'}</span>
<span>·</span>
<span className="truncate">{p.projectName}</span>
</div>
{p.selectedSupplierName && (
<div className="mt-0.5 truncate text-[11px] text-emerald-600">
{p.selectedSupplierName}
</div>
)}
</div>
<span
className={cn(
'shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium',
PurchaseEvaluationPhaseColor[p.phase],
)}
>
{PurchaseEvaluationPhaseLabel[p.phase]}
</span>
</div>
<div className="mt-1 flex items-center justify-between text-[11px] text-slate-500">
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-slate-600">
{PurchaseEvaluationTypeLabel[p.type]}
</span>
<SlaTimer deadline={p.slaDeadline} createdAt={p.createdAt} />
</div>
{p.contractId && (
<div className="mt-1 text-[10px] text-brand-600"> Đã tạo </div>
)}
</button>
</li>
))}
</ul>
</div>
{/* Sticky bottom — "+ Thêm mới" button (mirror HĐ Thầu phụ pattern) */}
{showCreateButton && (
<div className="border-t border-slate-200 bg-white p-2">
<Button
onClick={() => onCreate?.()}
className="w-full justify-center gap-1.5 text-xs"
>
<Plus className="h-3.5 w-3.5" /> Thêm mới
</Button>
</div>
)}
</aside>
)
}

View File

@ -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 <PeHeaderForm> · else
// <PeDetailTabs mode="workspace"> (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<PeDetailBundle>(`/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<string, string | null>) {
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 (
<div className="flex h-[calc(100vh-4rem)] flex-col">
<header className="flex shrink-0 flex-wrap items-center justify-between gap-3 border-b border-slate-200 bg-white px-6 py-3">
<div className="flex items-center gap-2">
<ClipboardCheck className="h-5 w-5 text-slate-500" />
<h1 className="text-base font-semibold tracking-tight text-slate-900">{headerTitle}</h1>
</div>
<div className="text-[12px] text-slate-500">
Workspace 2-panel Workflow + Duyệt menu &ldquo;Duyệt&rdquo;.
</div>
</header>
<div className="grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[320px_1fr]">
{/* Panel 1: List pure picker + sticky create */}
<PeListPanel
typeFilter={typeFilter}
selectedId={selectedId}
search={search}
phase={phase}
onSelect={id => setParams({ id, mode: null })}
onSearchChange={q => setParams({ q })}
onPhaseChange={p => setParams({ phase: p })}
showCreateButton
onCreate={() => setParams({ mode: 'new', id: null })}
/>
{/* Panel 2: Empty | Header form | Detail tabs (workspace mode) */}
<main className="hidden overflow-y-auto bg-slate-50 p-6 lg:block">
{/* Empty: chưa pick + chưa create */}
{!selectedId && mode !== 'new' && (
<EmptyState
icon={ClipboardCheck}
title="Chọn phiếu hoặc tạo mới"
description='Chọn 1 phiếu ở danh sách trái để nhập liệu, hoặc bấm "+ Thêm mới" ở dưới.'
/>
)}
{/* Mode "new": header form */}
{mode === 'new' && (
<PeHeaderForm
defaultType={typeFilter}
onSaved={(newId, t) => setParams({ id: newId, mode: null, type: String(t) })}
onCancel={() => setParams({ mode: null })}
/>
)}
{/* Mode "edit": detail tabs (workspace = no workflow + Section 5 disabled) */}
{selectedId && detail.isLoading && (
<div className="text-sm text-slate-500">Đang tải</div>
)}
{selectedId && detail.data && (
<PeDetailTabs
evaluation={detail.data}
onBack={() => setParams({ id: null })}
onDelete={() => del.mutate(detail.data!.id)}
mode="workspace"
/>
)}
</main>
</div>
{/* Mobile fallback: nếu không lg, redirect về detail page */}
{selectedId && (
<div className="lg:hidden">
{/* Quick UX: tap row khi mobile sẽ navigate fullpage detail */}
<button
onClick={() => navigate(`/purchase-evaluations/${selectedId}`)}
className="hidden"
/>
</div>
)}
</div>
)
}