[CLAUDE] FE-Admin: PE InfoTab inline edit Section 1 + PeListPanel pencil edit hover
User feedback 2026-05-07: muốn thêm nút edit kế bên row trong Panel 1, click →
Panel 2 sáng nội dung Section 1 lên cho user sửa header inline (KHÔNG cần đi
"Sửa header" page). Cũng muốn create new interface gần giống detail view
sectioned (defer cho chunk sau, hoặc keep PeHeaderForm nếu user OK).
Implementation:
~ PeDetailTabs.tsx
- InfoTab thêm prop `readOnly` + `autoEdit` (trigger edit mode tự động khi
mount nếu URL ?editHeader=1)
- canEdit = !readOnly && isDraft (DangSoanThao):
→ display mode: hiển thị FormRow + button "✎ Sửa" góc trên phải Section 1
→ editing mode (click Sửa hoặc autoEdit): card border brand-200 + 4 input
(Tên * / Dự án disabled / Địa điểm / Mô tả / Payment) + nút Lưu/Hủy
- Save: PUT /pe/:id full payload (current ev values + new editable fields).
onSuccess: invalidate ['pe-detail', 'pe-list'] + setEditing(false)
- PeDetailTabs prop `autoEditHeader` mới — pass-through xuống InfoTab
~ PeListPanel.tsx
- Thêm prop `onEditClick?: (id) => void`
- Pencil icon (lucide) absolute right-2 top-2 trong mỗi <li>, opacity-0
group-hover:opacity-100 — chỉ hiện khi user hover row + onEditClick set
- Click → trigger onEditClick(id) callback (different from row click)
~ PurchaseEvaluationWorkspacePage.tsx
- Đọc URL ?editHeader=1 → pass autoEditHeader xuống PeDetailTabs
- PeListPanel onEditClick → setParams({ id, mode: null, editHeader: '1' })
- onSelect (click row thường) → editHeader: null (clear flag)
- onBack → clear editHeader
Verify: npm run build fe-admin pass · 0 TS error.
Pending: workspace "new" mode wrap PeHeaderForm trong sectioned layout giống
detail view (defer — user có thể chấp nhận PeHeaderForm hiện tại nếu OK).
Next: Chunk 2 fe-user mirror.
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