- {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 (
+
+
+
+
+
+
+
)
}
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}
/>
)}