From 5a89dd218881ff2d7c5f399f7211f4ff3e7a9d5b Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Thu, 7 May 2026 14:55:49 +0700 Subject: [PATCH] [CLAUDE] FE-Admin: PE InfoTab inline edit Section 1 + PeListPanel pencil edit hover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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
  • , 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) --- fe-admin/src/components/pe/PeDetailTabs.tsx | 149 ++++++++++++++++-- fe-admin/src/components/pe/PeListPanel.tsx | 19 ++- .../pe/PurchaseEvaluationWorkspacePage.tsx | 11 +- 3 files changed, 160 insertions(+), 19 deletions(-) diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index 233c7ce..c57d238 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/src/components/pe/PeDetailTabs.tsx @@ -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({
    {/* Section 1 — đúng spec form FO-PHIẾU TRÌNH KÝ CHỌN TP/NCC */}
    - +
    @@ -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 ( -
    - - - {(ev.diaDiem || ev.moTa) && ( -
    - {ev.diaDiem &&
    Địa điểm: {ev.diaDiem}
    } - {ev.moTa &&
    Mô tả: {ev.moTa}
    } +// 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 ( +
    +
    + + {canEdit && ( + + )}
    - )} -
    + + {(ev.diaDiem || ev.moTa || ev.paymentTerms) && ( +
    + {ev.diaDiem &&
    Địa điểm: {ev.diaDiem}
    } + {ev.moTa &&
    Mô tả: {ev.moTa}
    } + {ev.paymentTerms &&
    Điều khoản TT: {ev.paymentTerms}
    } +
    + )} +
    + ) + } + + // Editing mode + return ( +
    +
    +
    + + setTenGoiThau(e.target.value)} + placeholder="vd Cung cấp bê tông" + /> +
    +
    + + +
    +
    + + setDiaDiem(e.target.value)} + placeholder="Lô K, KCN Lộc An..." + /> +
    +
    + + setMoTa(e.target.value)} + placeholder="Phương án A: ..." + /> +
    +
    + + setPaymentTerms(e.target.value)} + placeholder="JSON hoặc text" + /> +
    +
    +
    + + +
    +
    ) } diff --git a/fe-admin/src/components/pe/PeListPanel.tsx b/fe-admin/src/components/pe/PeListPanel.tsx index 9321f82..89ad018 100644 --- a/fe-admin/src/components/pe/PeListPanel.tsx +++ b/fe-admin/src/components/pe/PeListPanel.tsx @@ -6,7 +6,7 @@ // 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 { ClipboardCheck, Pencil, Plus, Search } from 'lucide-react' import { Button } from '@/components/ui/Button' import { Input } from '@/components/ui/Input' import { Select } from '@/components/ui/Select' @@ -34,6 +34,7 @@ export function PeListPanel({ onPhaseChange, showCreateButton = false, onCreate, + onEditClick, }: { typeFilter: number | null pendingMe?: boolean @@ -45,6 +46,8 @@ export function PeListPanel({ onPhaseChange: (p: string) => void showCreateButton?: boolean onCreate?: () => void + /** Pencil edit icon hover next-to-row — click → select + auto-open Section 1 edit mode (URL ?editHeader=1). */ + onEditClick?: (id: string) => void }) { const list = useQuery({ queryKey: ['pe-list', { typeFilter, pendingMe, search, phase }], @@ -122,11 +125,11 @@ export function PeListPanel({ )}
      {rows.map(p => ( -
    • +
    • + {/* Edit pencil — visible on hover (chỉ khi onEditClick được truyền) */} + {onEditClick && ( + + )}
    • ))}
    diff --git a/fe-admin/src/pages/pe/PurchaseEvaluationWorkspacePage.tsx b/fe-admin/src/pages/pe/PurchaseEvaluationWorkspacePage.tsx index c7eddb6..d2c4d54 100644 --- a/fe-admin/src/pages/pe/PurchaseEvaluationWorkspacePage.tsx +++ b/fe-admin/src/pages/pe/PurchaseEvaluationWorkspacePage.tsx @@ -34,6 +34,7 @@ export function PurchaseEvaluationWorkspacePage() { const phase = sp.get('phase') ?? '' const selectedId = sp.get('id') const mode = sp.get('mode') // 'new' | null + const autoEditHeader = sp.get('editHeader') === '1' const detail = useQuery({ queryKey: ['pe-detail', selectedId], @@ -77,17 +78,18 @@ export function PurchaseEvaluationWorkspacePage() {
    - {/* Panel 1: List pure picker + sticky create */} + {/* Panel 1: List pure picker + sticky create + pencil edit hover */} setParams({ id, mode: null })} + onSelect={id => setParams({ id, mode: null, editHeader: null })} onSearchChange={q => setParams({ q })} onPhaseChange={p => setParams({ phase: p })} showCreateButton - onCreate={() => setParams({ mode: 'new', id: null })} + onCreate={() => setParams({ mode: 'new', id: null, editHeader: null })} + onEditClick={id => setParams({ id, mode: null, editHeader: '1' })} /> {/* Panel 2: Empty | Header form | Detail tabs (workspace mode) */} @@ -117,9 +119,10 @@ export function PurchaseEvaluationWorkspacePage() { {selectedId && detail.data && ( setParams({ id: null })} + onBack={() => setParams({ id: null, editHeader: null })} onDelete={() => del.mutate(detail.data!.id)} mode="workspace" + autoEditHeader={autoEditHeader} /> )}