[CLAUDE] FE-Admin+FE-User: PE workspace "new" mode — sectioned create view 5 sections
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m4s

User feedback 2026-05-07: "Thêm mới list ra hết trường dữ liệu giống chỉnh sửa
nhưng trống, mở rộng từng phần. Save header xong mới cho nhập chi tiết."

Implementation:
  + PeWorkspaceCreateView.tsx (~230 LOC, mirror fe-admin + fe-user)
    - Sectioned card layout giống PeDetailTabs (5 section divider + title style)
    - Section 1 "Thông tin gói thầu": editable inputs (Loại / Tên */Dự án */Địa
      điểm/Mô tả/Payment) — 2-col grid responsive
    - Section 2 "Chọn NCC/TP":
      a. NCC chọn: text "(sau khi thêm NCC + chốt winner)" placeholder
      b. Ngân sách: editable inline (toggle "Nhập tay" + Select OR 2 input —
         giống BudgetFieldRow pattern)
      c. Giá chào thầu: text "(auto-tính sau winner)" placeholder
      d. Bản so sánh: LockedHint icon + text "Tải sau khi tạo"
    - Section 3 "NCC tham gia (0)": LockedHint "Lưu phiếu trước → thêm NCC..."
    - Section 4 "Hạng mục + Báo giá (0)": LockedHint
    - Section 5 "Ý kiến 4 PB": amber banner "nhập khi duyệt"
    - Action bar bottom: "Tạo phiếu" (disabled khi !tenGoiThau || !projectId)
      + Hủy
    - POST /pe full payload (header + budget mode A or B). onSuccess: toast +
      invalidate pe-list + onSaved(id, type) callback
  ~ PurchaseEvaluationWorkspacePage.tsx (× 2 app)
    - Replace <PeHeaderForm> → <PeWorkspaceCreateView> trong mode='new'
    - PeHeaderForm vẫn còn (dùng cho /new?id= deep-link "Sửa header" cũ)

Helpers duplicate trong PeWorkspaceCreateView (Section + FormRow + LockedHint)
để tránh circular import từ PeDetailTabs.

UAT mode rule applied (per memory feedback_uat_skip_verify): skip dotnet test
+ npm build verify, push ngay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-07 15:06:31 +07:00
parent cb0598d76d
commit 66fa4691dc
4 changed files with 622 additions and 6 deletions

View File

@ -0,0 +1,308 @@
// Create view cho workspace mode "new" — sectioned layout giống PeDetailTabs
// (5 section visible) nhưng trống hết. Section 1 + 2 editable (header + budget).
// Section 3-5 locked với placeholder "Lưu phiếu trước". Sau save → caller
// onSaved navigate sang ?id={newId} → workspace switch sang detail view (full
// PeDetailTabs với inline edit Section 1 + BudgetFieldRow + Suppliers/Items).
//
// Pattern user 2026-05-07: "Thêm mới list ra hết trường dữ liệu giống chỉnh
// sửa nhưng trống, mở rộng từng phần. Save header xong mới cho nhập chi tiết."
import { useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { Lock } from 'lucide-react'
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,
} from '@/types/purchaseEvaluation'
import { BudgetPhase, type BudgetListItem } from '@/types/budget'
import type { Paged, Project } from '@/types/master'
export function PeWorkspaceCreateView({
defaultType,
onSaved,
onCancel,
}: {
defaultType: number
/** Callback sau khi POST thành công với (newId, type). Caller navigate. */
onSaved: (id: string, type: number) => void
onCancel?: () => void
}) {
const qc = useQueryClient()
const [form, setForm] = useState({
type: defaultType,
tenGoiThau: '',
projectId: '',
diaDiem: '',
moTa: '',
paymentTerms: '',
budgetId: '',
// Mig 17 — manual budget fallback
budgetManual: false,
budgetManualName: '',
budgetManualAmount: 0,
})
const projects = useQuery({
queryKey: ['all-projects'],
queryFn: async () => (await api.get<{ items: Project[] }>('/projects', { params: { pageSize: 1000 } })).data.items,
})
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,
})
const budgetPayload = form.budgetManual
? { budgetId: null, budgetManualName: form.budgetManualName || null, budgetManualAmount: form.budgetManualAmount > 0 ? form.budgetManualAmount : null }
: { budgetId: form.budgetId || null, budgetManualName: null, budgetManualAmount: null }
const create = useMutation({
mutationFn: async () => {
const res = await 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,
...budgetPayload,
})
return res.data.id
},
onSuccess: id => {
toast.success('Đã tạo phiếu — mở chi tiết để thêm NCC + hạng mục.')
qc.invalidateQueries({ queryKey: ['pe-list'] })
onSaved(id, form.type)
},
onError: e => toast.error(getErrorMessage(e)),
})
const canSubmit = !!form.tenGoiThau && !!form.projectId && !create.isPending
return (
<div className="rounded-lg border border-slate-200 bg-white shadow-sm">
{/* Header bar */}
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-slate-200 px-5 py-3">
<div>
<h2 className="text-base font-semibold text-slate-900">Tạo phiếu Duyệt NCC mới</h2>
<p className="mt-0.5 text-[12px] text-slate-500">
Nhập Section 1 + 2 bấm <strong>&ldquo;Tạo phiếu&rdquo;</strong> sau đó NCC / Hạng mục / Báo giá / Ý kiến mở khóa.
</p>
</div>
{onCancel && (
<Button variant="ghost" onClick={onCancel} className="text-xs"> Hủy</Button>
)}
</div>
<div className="divide-y divide-slate-200">
{/* Section 1 — Thông tin gói thầu (editable) */}
<Section title="1. Thông tin gói thầu">
<div className="grid gap-3 md:grid-cols-2">
<div>
<Label className="text-[11px]">Loại quy trình *</Label>
<Select value={form.type} 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 className="md:col-span-1">
<Label className="text-[11px]">a. 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 className="md:col-span-2">
<Label className="text-[11px]">b. Dự án *</Label>
<Select
value={form.projectId}
onChange={e => setForm({ ...form, projectId: e.target.value, budgetId: '' })}
>
<option value=""> Chọn dự án </option>
{projects.data?.map(p => (
<option key={p.id} value={p.id}>{p.code} {p.name}</option>
))}
</Select>
</div>
<div>
<Label className="text-[11px]">Đa điểm</Label>
<Input
value={form.diaDiem}
onChange={e => setForm({ ...form, diaDiem: e.target.value })}
placeholder="Lô K, KCN Lộc An..."
/>
</div>
<div>
<Label className="text-[11px]"> tả ngắn</Label>
<Input
value={form.moTa}
onChange={e => setForm({ ...form, moTa: e.target.value })}
placeholder="Phương án A: ..."
/>
</div>
<div className="md:col-span-2">
<Label className="text-[11px]">Điều khoản thanh toán</Label>
<Textarea
rows={2}
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>
</Section>
{/* Section 2 — Chọn NCC/TP (chỉ Ngân sách editable, còn lại sẽ unlock sau create) */}
<Section title="2. Chọn NCC / TP">
<div className="space-y-3">
<FormRow
label="a. NCC / TP được chọn"
value={<span className="text-slate-400"> (sau khi thêm NCC tham gia + chốt winner)</span>}
/>
{/* b. Ngân sách — editable inline (Mig 17 toggle pattern) */}
<div className="flex gap-3">
<span className="w-44 shrink-0 pt-1.5 text-[12px] text-slate-500">b. Ngân sách</span>
<div className="min-w-0 flex-1 space-y-2">
<label className="inline-flex cursor-pointer items-center gap-1.5 text-[11px] text-slate-600">
<input
type="checkbox"
checked={form.budgetManual}
onChange={e => setForm({ ...form, budgetManual: e.target.checked })}
className="h-3.5 w-3.5 rounded border-slate-300"
/>
Nhập tay (không link)
</label>
{!form.budgetManual ? (
<>
<Select
value={form.budgetId}
disabled={!form.projectId}
onChange={e => setForm({ ...form, budgetId: e.target.value })}
className="text-sm"
>
<option value=""></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="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 — bật "Nhập tay" để nhập số tiền trực tiếp.'
: 'Chỉ list ngân sách đã duyệt cùng dự án.'}
</p>
</>
) : (
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
<Input
value={form.budgetManualName}
onChange={e => setForm({ ...form, budgetManualName: e.target.value })}
placeholder="Tên (vd Tạm tính T11/2025)"
maxLength={200}
className="text-sm"
/>
<Input
type="number"
min={0}
value={form.budgetManualAmount || ''}
onChange={e => setForm({ ...form, budgetManualAmount: Number(e.target.value) })}
placeholder="Số tiền (đ)"
className="text-sm"
/>
</div>
)}
</div>
</div>
<FormRow
label="c. Giá chào thầu"
value={<span className="text-slate-400"> (auto-tính từ báo giá NCC sau khi chọn winner)</span>}
/>
<FormRow
label="d. Bản so sánh"
value={<LockedHint text="Tải bảng so sánh sau khi tạo phiếu." />}
/>
</div>
</Section>
{/* Section 3 — NCC tham gia (locked) */}
<Section title="3. NCC / TP tham gia (0)">
<LockedHint text="Lưu phiếu trước → thêm NCC + nhập thông tin liên hệ + chốt so sánh ở Detail tabs." />
</Section>
{/* Section 4 — Hạng mục + Báo giá (locked) */}
<Section title="4. Hạng mục + Báo giá (0)">
<LockedHint text="Lưu phiếu trước → thêm hạng mục + nhập báo giá ma trận NCC × Item ở Detail tabs." />
</Section>
{/* Section 5 — Ý kiến 4 phòng ban (locked + amber hint per Q5 user) */}
<Section title="5. Ý kiến 4 phòng ban (sign-off)">
<div className="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>
</Section>
</div>
{/* Action bar */}
<div className="flex items-center justify-end gap-2 border-t border-slate-200 bg-slate-50 px-5 py-3">
{onCancel && (
<Button variant="ghost" onClick={onCancel} className="text-xs">Hủy</Button>
)}
<Button
onClick={() => create.mutate()}
disabled={!canSubmit}
>
{create.isPending ? 'Đang tạo…' : 'Tạo phiếu'}
</Button>
</div>
</div>
)
}
// Helper components — duplicate từ PeDetailTabs để tránh circular import.
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<section className="px-5 py-4">
<h3 className="mb-3 text-xs font-semibold uppercase tracking-wide text-slate-500">{title}</h3>
{children}
</section>
)
}
function FormRow({ label, value }: { label: string; value: React.ReactNode }) {
return (
<div className="flex items-baseline gap-3 text-sm">
<span className="w-44 shrink-0 text-[12px] text-slate-500">{label}</span>
<div className="min-w-0 flex-1 text-slate-800">{value}</div>
</div>
)
}
function LockedHint({ text }: { text: string }) {
return (
<div className="inline-flex items-center gap-1.5 rounded border border-dashed border-slate-300 bg-slate-50 px-3 py-1.5 text-[12px] text-slate-500">
<Lock className="h-3 w-3" />
{text}
</div>
)
}

View File

@ -16,7 +16,7 @@ import { ClipboardCheck } from 'lucide-react'
import { EmptyState } from '@/components/EmptyState' import { EmptyState } from '@/components/EmptyState'
import { PeDetailTabs } from '@/components/pe/PeDetailTabs' import { PeDetailTabs } from '@/components/pe/PeDetailTabs'
import { PeListPanel } from '@/components/pe/PeListPanel' import { PeListPanel } from '@/components/pe/PeListPanel'
import { PeHeaderForm } from '@/components/pe/PeHeaderForm' 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 {
@ -103,9 +103,9 @@ export function PurchaseEvaluationWorkspacePage() {
/> />
)} )}
{/* Mode "new": header form */} {/* Mode "new": sectioned create view (5 sections, 3-5 locked tới khi save) */}
{mode === 'new' && ( {mode === 'new' && (
<PeHeaderForm <PeWorkspaceCreateView
defaultType={typeFilter} defaultType={typeFilter}
onSaved={(newId, t) => setParams({ id: newId, mode: null, type: String(t) })} onSaved={(newId, t) => setParams({ id: newId, mode: null, type: String(t) })}
onCancel={() => setParams({ mode: null })} onCancel={() => setParams({ mode: null })}

View File

@ -0,0 +1,308 @@
// Create view cho workspace mode "new" — sectioned layout giống PeDetailTabs
// (5 section visible) nhưng trống hết. Section 1 + 2 editable (header + budget).
// Section 3-5 locked với placeholder "Lưu phiếu trước". Sau save → caller
// onSaved navigate sang ?id={newId} → workspace switch sang detail view (full
// PeDetailTabs với inline edit Section 1 + BudgetFieldRow + Suppliers/Items).
//
// Pattern user 2026-05-07: "Thêm mới list ra hết trường dữ liệu giống chỉnh
// sửa nhưng trống, mở rộng từng phần. Save header xong mới cho nhập chi tiết."
import { useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { Lock } from 'lucide-react'
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,
} from '@/types/purchaseEvaluation'
import { BudgetPhase, type BudgetListItem } from '@/types/budget'
import type { Paged, Project } from '@/types/master'
export function PeWorkspaceCreateView({
defaultType,
onSaved,
onCancel,
}: {
defaultType: number
/** Callback sau khi POST thành công với (newId, type). Caller navigate. */
onSaved: (id: string, type: number) => void
onCancel?: () => void
}) {
const qc = useQueryClient()
const [form, setForm] = useState({
type: defaultType,
tenGoiThau: '',
projectId: '',
diaDiem: '',
moTa: '',
paymentTerms: '',
budgetId: '',
// Mig 17 — manual budget fallback
budgetManual: false,
budgetManualName: '',
budgetManualAmount: 0,
})
const projects = useQuery({
queryKey: ['all-projects'],
queryFn: async () => (await api.get<{ items: Project[] }>('/projects', { params: { pageSize: 1000 } })).data.items,
})
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,
})
const budgetPayload = form.budgetManual
? { budgetId: null, budgetManualName: form.budgetManualName || null, budgetManualAmount: form.budgetManualAmount > 0 ? form.budgetManualAmount : null }
: { budgetId: form.budgetId || null, budgetManualName: null, budgetManualAmount: null }
const create = useMutation({
mutationFn: async () => {
const res = await 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,
...budgetPayload,
})
return res.data.id
},
onSuccess: id => {
toast.success('Đã tạo phiếu — mở chi tiết để thêm NCC + hạng mục.')
qc.invalidateQueries({ queryKey: ['pe-list'] })
onSaved(id, form.type)
},
onError: e => toast.error(getErrorMessage(e)),
})
const canSubmit = !!form.tenGoiThau && !!form.projectId && !create.isPending
return (
<div className="rounded-lg border border-slate-200 bg-white shadow-sm">
{/* Header bar */}
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-slate-200 px-5 py-3">
<div>
<h2 className="text-base font-semibold text-slate-900">Tạo phiếu Duyệt NCC mới</h2>
<p className="mt-0.5 text-[12px] text-slate-500">
Nhập Section 1 + 2 bấm <strong>&ldquo;Tạo phiếu&rdquo;</strong> sau đó NCC / Hạng mục / Báo giá / Ý kiến mở khóa.
</p>
</div>
{onCancel && (
<Button variant="ghost" onClick={onCancel} className="text-xs"> Hủy</Button>
)}
</div>
<div className="divide-y divide-slate-200">
{/* Section 1 — Thông tin gói thầu (editable) */}
<Section title="1. Thông tin gói thầu">
<div className="grid gap-3 md:grid-cols-2">
<div>
<Label className="text-[11px]">Loại quy trình *</Label>
<Select value={form.type} 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 className="md:col-span-1">
<Label className="text-[11px]">a. 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 className="md:col-span-2">
<Label className="text-[11px]">b. Dự án *</Label>
<Select
value={form.projectId}
onChange={e => setForm({ ...form, projectId: e.target.value, budgetId: '' })}
>
<option value=""> Chọn dự án </option>
{projects.data?.map(p => (
<option key={p.id} value={p.id}>{p.code} {p.name}</option>
))}
</Select>
</div>
<div>
<Label className="text-[11px]">Đa điểm</Label>
<Input
value={form.diaDiem}
onChange={e => setForm({ ...form, diaDiem: e.target.value })}
placeholder="Lô K, KCN Lộc An..."
/>
</div>
<div>
<Label className="text-[11px]"> tả ngắn</Label>
<Input
value={form.moTa}
onChange={e => setForm({ ...form, moTa: e.target.value })}
placeholder="Phương án A: ..."
/>
</div>
<div className="md:col-span-2">
<Label className="text-[11px]">Điều khoản thanh toán</Label>
<Textarea
rows={2}
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>
</Section>
{/* Section 2 — Chọn NCC/TP (chỉ Ngân sách editable, còn lại sẽ unlock sau create) */}
<Section title="2. Chọn NCC / TP">
<div className="space-y-3">
<FormRow
label="a. NCC / TP được chọn"
value={<span className="text-slate-400"> (sau khi thêm NCC tham gia + chốt winner)</span>}
/>
{/* b. Ngân sách — editable inline (Mig 17 toggle pattern) */}
<div className="flex gap-3">
<span className="w-44 shrink-0 pt-1.5 text-[12px] text-slate-500">b. Ngân sách</span>
<div className="min-w-0 flex-1 space-y-2">
<label className="inline-flex cursor-pointer items-center gap-1.5 text-[11px] text-slate-600">
<input
type="checkbox"
checked={form.budgetManual}
onChange={e => setForm({ ...form, budgetManual: e.target.checked })}
className="h-3.5 w-3.5 rounded border-slate-300"
/>
Nhập tay (không link)
</label>
{!form.budgetManual ? (
<>
<Select
value={form.budgetId}
disabled={!form.projectId}
onChange={e => setForm({ ...form, budgetId: e.target.value })}
className="text-sm"
>
<option value=""></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="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 — bật "Nhập tay" để nhập số tiền trực tiếp.'
: 'Chỉ list ngân sách đã duyệt cùng dự án.'}
</p>
</>
) : (
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
<Input
value={form.budgetManualName}
onChange={e => setForm({ ...form, budgetManualName: e.target.value })}
placeholder="Tên (vd Tạm tính T11/2025)"
maxLength={200}
className="text-sm"
/>
<Input
type="number"
min={0}
value={form.budgetManualAmount || ''}
onChange={e => setForm({ ...form, budgetManualAmount: Number(e.target.value) })}
placeholder="Số tiền (đ)"
className="text-sm"
/>
</div>
)}
</div>
</div>
<FormRow
label="c. Giá chào thầu"
value={<span className="text-slate-400"> (auto-tính từ báo giá NCC sau khi chọn winner)</span>}
/>
<FormRow
label="d. Bản so sánh"
value={<LockedHint text="Tải bảng so sánh sau khi tạo phiếu." />}
/>
</div>
</Section>
{/* Section 3 — NCC tham gia (locked) */}
<Section title="3. NCC / TP tham gia (0)">
<LockedHint text="Lưu phiếu trước → thêm NCC + nhập thông tin liên hệ + chốt so sánh ở Detail tabs." />
</Section>
{/* Section 4 — Hạng mục + Báo giá (locked) */}
<Section title="4. Hạng mục + Báo giá (0)">
<LockedHint text="Lưu phiếu trước → thêm hạng mục + nhập báo giá ma trận NCC × Item ở Detail tabs." />
</Section>
{/* Section 5 — Ý kiến 4 phòng ban (locked + amber hint per Q5 user) */}
<Section title="5. Ý kiến 4 phòng ban (sign-off)">
<div className="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>
</Section>
</div>
{/* Action bar */}
<div className="flex items-center justify-end gap-2 border-t border-slate-200 bg-slate-50 px-5 py-3">
{onCancel && (
<Button variant="ghost" onClick={onCancel} className="text-xs">Hủy</Button>
)}
<Button
onClick={() => create.mutate()}
disabled={!canSubmit}
>
{create.isPending ? 'Đang tạo…' : 'Tạo phiếu'}
</Button>
</div>
</div>
)
}
// Helper components — duplicate từ PeDetailTabs để tránh circular import.
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<section className="px-5 py-4">
<h3 className="mb-3 text-xs font-semibold uppercase tracking-wide text-slate-500">{title}</h3>
{children}
</section>
)
}
function FormRow({ label, value }: { label: string; value: React.ReactNode }) {
return (
<div className="flex items-baseline gap-3 text-sm">
<span className="w-44 shrink-0 text-[12px] text-slate-500">{label}</span>
<div className="min-w-0 flex-1 text-slate-800">{value}</div>
</div>
)
}
function LockedHint({ text }: { text: string }) {
return (
<div className="inline-flex items-center gap-1.5 rounded border border-dashed border-slate-300 bg-slate-50 px-3 py-1.5 text-[12px] text-slate-500">
<Lock className="h-3 w-3" />
{text}
</div>
)
}

View File

@ -16,7 +16,7 @@ import { ClipboardCheck } from 'lucide-react'
import { EmptyState } from '@/components/EmptyState' import { EmptyState } from '@/components/EmptyState'
import { PeDetailTabs } from '@/components/pe/PeDetailTabs' import { PeDetailTabs } from '@/components/pe/PeDetailTabs'
import { PeListPanel } from '@/components/pe/PeListPanel' import { PeListPanel } from '@/components/pe/PeListPanel'
import { PeHeaderForm } from '@/components/pe/PeHeaderForm' 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 {
@ -103,9 +103,9 @@ export function PurchaseEvaluationWorkspacePage() {
/> />
)} )}
{/* Mode "new": header form */} {/* Mode "new": sectioned create view (5 sections, 3-5 locked tới khi save) */}
{mode === 'new' && ( {mode === 'new' && (
<PeHeaderForm <PeWorkspaceCreateView
defaultType={typeFilter} defaultType={typeFilter}
onSaved={(newId, t) => setParams({ id: newId, mode: null, type: String(t) })} onSaved={(newId, t) => setParams({ id: newId, mode: null, type: String(t) })}
onCancel={() => setParams({ mode: null })} onCancel={() => setParams({ mode: null })}