[CLAUDE] FE-User: PE InfoTab inline edit + PeListPanel pencil edit hover mirror
Chunk 2/3 — mirror y hệt Chunk 1 sang fe-user (rule §3.9). 3 file: ~ components/pe/PeDetailTabs.tsx — InfoTab inline edit + autoEditHeader prop ~ components/pe/PeListPanel.tsx — pencil icon group-hover absolute right ~ pages/pe/PurchaseEvaluationWorkspacePage.tsx — URL editHeader=1 wiring Verify: npm run build fe-user pass · 0 TS error. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -53,6 +53,7 @@ export function PeDetailTabs({
|
||||
onDelete,
|
||||
readOnly = false,
|
||||
mode = 'detail',
|
||||
autoEditHeader = false,
|
||||
}: {
|
||||
evaluation: PeDetailBundle
|
||||
onBack: () => void
|
||||
@ -61,6 +62,8 @@ export function PeDetailTabs({
|
||||
readOnly?: boolean
|
||||
/** 'workspace' = Section 5 LUÔN disabled (ý kiến nhập ở leaf Duyệt). */
|
||||
mode?: 'detail' | 'workspace'
|
||||
/** Auto open Section 1 InfoTab in edit mode khi mount — triggered từ pencil icon Panel 1 */
|
||||
autoEditHeader?: boolean
|
||||
}) {
|
||||
const navigate = useNavigate()
|
||||
const isDraft = evaluation.phase === PurchaseEvaluationPhase.DangSoanThao
|
||||
@ -113,7 +116,7 @@ export function PeDetailTabs({
|
||||
<div className="divide-y divide-slate-200">
|
||||
{/* Section 1 — đúng spec form FO-PHIẾU TRÌNH KÝ CHỌN TP/NCC */}
|
||||
<Section title="1. Thông tin gói thầu">
|
||||
<InfoTab ev={evaluation} />
|
||||
<InfoTab ev={evaluation} readOnly={readOnly} autoEdit={autoEditHeader} />
|
||||
</Section>
|
||||
<Section title="2. Chọn NCC / TP">
|
||||
<ChonNccSection ev={evaluation} readOnly={readOnly} />
|
||||
@ -292,18 +295,140 @@ export function PeHistorySection({ ev }: { ev: PeDetailBundle }) {
|
||||
}
|
||||
|
||||
// ===== Section 1 — Thông tin gói thầu (spec: a. Tên gói thầu / b. Dự án) =====
|
||||
function InfoTab({ ev }: { ev: PeDetailBundle }) {
|
||||
return (
|
||||
<dl className="space-y-2 text-sm">
|
||||
<FormRow label="a. Tên gói thầu" value={ev.tenGoiThau} />
|
||||
<FormRow label="b. Dự án" value={ev.projectName} />
|
||||
{(ev.diaDiem || ev.moTa) && (
|
||||
<div className="mt-3 rounded bg-slate-50 px-3 py-2 text-[12px] text-slate-600">
|
||||
{ev.diaDiem && <div><span className="text-slate-400">Địa điểm:</span> {ev.diaDiem}</div>}
|
||||
{ev.moTa && <div><span className="text-slate-400">Mô tả:</span> {ev.moTa}</div>}
|
||||
// Inline editable khi canEdit (=!readOnly && isDraft). Edit pencil button "Sửa"
|
||||
// flip display ↔ form mode. Save dùng existing PUT /pe/:id endpoint với current
|
||||
// entity values + new header fields. Dự án + Type LOCKED sau create — chỉ Tên/
|
||||
// Địa điểm/Mô tả/Payment editable inline. autoEdit prop cho phép trigger edit
|
||||
// mode từ pencil icon trong PeListPanel (URL flag ?editHeader=1).
|
||||
function InfoTab({ ev, readOnly, autoEdit }: { ev: PeDetailBundle; readOnly: boolean; autoEdit: boolean }) {
|
||||
const isDraft = ev.phase === PurchaseEvaluationPhase.DangSoanThao
|
||||
const canEdit = !readOnly && isDraft
|
||||
const qc = useQueryClient()
|
||||
const [editing, setEditing] = useState(autoEdit && canEdit)
|
||||
const [tenGoiThau, setTenGoiThau] = useState(ev.tenGoiThau)
|
||||
const [diaDiem, setDiaDiem] = useState(ev.diaDiem ?? '')
|
||||
const [moTa, setMoTa] = useState(ev.moTa ?? '')
|
||||
const [paymentTerms, setPaymentTerms] = useState(ev.paymentTerms ?? '')
|
||||
|
||||
const dirty = tenGoiThau !== ev.tenGoiThau
|
||||
|| diaDiem !== (ev.diaDiem ?? '')
|
||||
|| moTa !== (ev.moTa ?? '')
|
||||
|| paymentTerms !== (ev.paymentTerms ?? '')
|
||||
|
||||
const save = useMutation({
|
||||
mutationFn: async () => {
|
||||
await api.put(`/purchase-evaluations/${ev.id}`, {
|
||||
id: ev.id,
|
||||
tenGoiThau,
|
||||
diaDiem: diaDiem || null,
|
||||
moTa: moTa || null,
|
||||
paymentTerms: paymentTerms || null,
|
||||
budgetId: ev.budgetId,
|
||||
budgetManualName: ev.budgetManualName,
|
||||
budgetManualAmount: ev.budgetManualAmount,
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('Đã cập nhật thông tin')
|
||||
qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] })
|
||||
qc.invalidateQueries({ queryKey: ['pe-list'] })
|
||||
setEditing(false)
|
||||
},
|
||||
onError: e => toast.error(getErrorMessage(e)),
|
||||
})
|
||||
|
||||
function reset() {
|
||||
setTenGoiThau(ev.tenGoiThau)
|
||||
setDiaDiem(ev.diaDiem ?? '')
|
||||
setMoTa(ev.moTa ?? '')
|
||||
setPaymentTerms(ev.paymentTerms ?? '')
|
||||
}
|
||||
|
||||
if (!editing) {
|
||||
return (
|
||||
<dl className="space-y-2 text-sm">
|
||||
<div className="flex items-start justify-between">
|
||||
<FormRow label="a. Tên gói thầu" value={ev.tenGoiThau} />
|
||||
{canEdit && (
|
||||
<button
|
||||
onClick={() => setEditing(true)}
|
||||
className="inline-flex items-center gap-1 rounded px-2 py-1 text-[11px] text-slate-500 hover:bg-slate-100 hover:text-brand-600"
|
||||
title="Sửa thông tin gói thầu"
|
||||
>
|
||||
<Pencil className="h-3 w-3" /> Sửa
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
<FormRow label="b. Dự án" value={ev.projectName} />
|
||||
{(ev.diaDiem || ev.moTa || ev.paymentTerms) && (
|
||||
<div className="mt-3 rounded bg-slate-50 px-3 py-2 text-[12px] text-slate-600">
|
||||
{ev.diaDiem && <div><span className="text-slate-400">Địa điểm:</span> {ev.diaDiem}</div>}
|
||||
{ev.moTa && <div><span className="text-slate-400">Mô tả:</span> {ev.moTa}</div>}
|
||||
{ev.paymentTerms && <div><span className="text-slate-400">Điều khoản TT:</span> <span className="whitespace-pre-wrap">{ev.paymentTerms}</span></div>}
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
)
|
||||
}
|
||||
|
||||
// Editing mode
|
||||
return (
|
||||
<div className="space-y-3 rounded border border-brand-200 bg-brand-50/30 p-3">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="md:col-span-2">
|
||||
<Label className="text-[11px]">a. Tên gói thầu *</Label>
|
||||
<Input
|
||||
value={tenGoiThau}
|
||||
onChange={e => setTenGoiThau(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 (khóa)</Label>
|
||||
<Input value={ev.projectName} disabled className="bg-slate-100" />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[11px]">Địa điểm</Label>
|
||||
<Input
|
||||
value={diaDiem}
|
||||
onChange={e => setDiaDiem(e.target.value)}
|
||||
placeholder="Lô K, KCN Lộc An..."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[11px]">Mô tả ngắn</Label>
|
||||
<Input
|
||||
value={moTa}
|
||||
onChange={e => setMoTa(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>
|
||||
<Input
|
||||
value={paymentTerms}
|
||||
onChange={e => setPaymentTerms(e.target.value)}
|
||||
placeholder="JSON hoặc text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => { reset(); setEditing(false) }}
|
||||
className="h-7 px-3 text-xs"
|
||||
>
|
||||
Hủy
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => save.mutate()}
|
||||
disabled={!dirty || !tenGoiThau || save.isPending}
|
||||
className="h-7 px-3 text-xs"
|
||||
>
|
||||
{save.isPending ? 'Đang lưu…' : 'Lưu'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user