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} /> )}