Compare commits
3 Commits
7f38c02ab3
...
cb0598d76d
| Author | SHA1 | Date | |
|---|---|---|---|
| cb0598d76d | |||
| 27b291ccea | |||
| 5a89dd2188 |
@ -1,6 +1,17 @@
|
|||||||
# HANDOFF — Brief 5 phút cho session tiếp theo
|
# HANDOFF — Brief 5 phút cho session tiếp theo
|
||||||
|
|
||||||
**Last updated:** 2026-05-07 (Session 11+ chốt — **PE BudgetFieldRow inline editor + 3 commit FE-only.**)
|
**Last updated:** 2026-05-07 (Session 11++ chốt — **PE InfoTab inline edit Section 1 + Panel 1 pencil hover. 3 commit FE-only.**)
|
||||||
|
|
||||||
|
## TL;DR Session 11++ housekeeping (07/05 — InfoTab inline edit + pencil hover)
|
||||||
|
|
||||||
|
User feedback after BudgetFieldRow: muốn thêm nút edit kế bên row trong Panel 1 list, click sáng nội dung Section 1 lên cho sửa header inline.
|
||||||
|
|
||||||
|
- ✅ **InfoTab inline edit** — display mode + button "✎ Sửa" góc phải Section 1. Editing mode: card border brand-200 + 4 input (Tên / Dự án locked / Địa điểm / Mô tả / Payment) + Save (PUT /pe/:id) / Hủy.
|
||||||
|
- ✅ **PeListPanel pencil hover** — icon absolute right-2 top-2 mỗi row, group-hover opacity-100. Click → callback onEditClick(id).
|
||||||
|
- ✅ **URL flag `?editHeader=1`** — set bởi pencil click → autoEditHeader prop chain xuống PeDetailTabs → InfoTab tự open editing mode mount-time.
|
||||||
|
- ✅ **fe-admin** Chunk 1 (`5a89dd2`) · **fe-user mirror** Chunk 2 (this).
|
||||||
|
|
||||||
|
**Defer (chưa làm):** Refactor workspace "new" mode wrap PeHeaderForm trong sectioned layout giống detail view (5 sections visible). PeHeaderForm hiện tại single-card đủ dùng — chỉ làm thêm khi user feedback rõ.
|
||||||
|
|
||||||
## TL;DR Session 11+ housekeeping (07/05 — inline budget editor)
|
## TL;DR Session 11+ housekeeping (07/05 — inline budget editor)
|
||||||
|
|
||||||
|
|||||||
@ -61,6 +61,7 @@
|
|||||||
|
|
||||||
| Ngày | Ai | Task | Commit |
|
| Ngày | Ai | Task | Commit |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
| 2026-05-07 | Claude | **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 sáng nội dung Section 1 lên cho user sửa header inline (KHÔNG cần đi "Sửa header" page). 2 chunk per-commit (build pass mỗi chunk): C1 fe-admin (3 file) — InfoTab thêm prop `readOnly + autoEdit`, canEdit=`!readOnly && isDraft`: display mode hiển thị FormRow + button "✎ Sửa" góc trên phải, editing mode card border brand-200 + 4 input (Tên */Dự án locked/Địa điểm/Mô tả/Payment) + Save (PUT /pe/:id full payload + invalidate detail+list)/Hủy. PeListPanel thêm prop `onEditClick`, pencil icon absolute right-2 top-2 mỗi row, opacity-0 group-hover:opacity-100. PurchaseEvaluationWorkspacePage đọc URL `?editHeader=1` → pass `autoEditHeader` xuống PeDetailTabs → trigger edit auto. C2 fe-user mirror y hệt 3 file (rule §3.9). KHÔNG đụng BE. KHÔNG refactor workspace "new" mode (defer — PeHeaderForm hiện tại đủ dùng, làm thêm khi user feedback). | `5a89dd2` (C1) · (current C2) · (current C3 docs) |
|
||||||
| 2026-05-07 | Claude | **PE BudgetFieldRow inline editor — toggle + 2 fields trong Section 2 (cả 3 view)** — User feedback after Session 11: muốn toggle "Nhập tay" + 2 input fields hiển thị trực tiếp trong Section 2 "b. Ngân sách" của PeDetailTabs — KHÔNG chỉ ở "Sửa header" page, mà cả 3 view (Workspace / Danh sách / Duyệt). Empty values cứ hiển thị empty (không text "(chưa link)" verbose). 2 chunk mirror per-commit (build pass mỗi chunk): C1 fe-admin BudgetFieldRow component (~125 LOC) thay FormRow tĩnh ở `b. Ngân sách`. canEdit=`!readOnly && isDraft`: render toggle + Select Budget OR 2 input grid 2-col + nút "Lưu ngân sách" (chỉ hiện khi dirty) + "Hủy thay đổi". Save: full PUT `/pe/:id` với current entity values + new budget payload. Read-only mode (Duyệt + !isDraft): chỉ display, KHÔNG toggle/inputs, empty hiển thị "—". C2 fe-user mirror y hệt 1 file (rule §3.9). Mỗi chunk: build pass + 0 TS error. KHÔNG đụng BE — re-use existing PUT endpoint. | `19712d8` (C1) · `d5c6f12` (C2) · (current C3) |
|
| 2026-05-07 | Claude | **PE BudgetFieldRow inline editor — toggle + 2 fields trong Section 2 (cả 3 view)** — User feedback after Session 11: muốn toggle "Nhập tay" + 2 input fields hiển thị trực tiếp trong Section 2 "b. Ngân sách" của PeDetailTabs — KHÔNG chỉ ở "Sửa header" page, mà cả 3 view (Workspace / Danh sách / Duyệt). Empty values cứ hiển thị empty (không text "(chưa link)" verbose). 2 chunk mirror per-commit (build pass mỗi chunk): C1 fe-admin BudgetFieldRow component (~125 LOC) thay FormRow tĩnh ở `b. Ngân sách`. canEdit=`!readOnly && isDraft`: render toggle + Select Budget OR 2 input grid 2-col + nút "Lưu ngân sách" (chỉ hiện khi dirty) + "Hủy thay đổi". Save: full PUT `/pe/:id` với current entity values + new budget payload. Read-only mode (Duyệt + !isDraft): chỉ display, KHÔNG toggle/inputs, empty hiển thị "—". C2 fe-user mirror y hệt 1 file (rule §3.9). Mỗi chunk: build pass + 0 TS error. KHÔNG đụng BE — re-use existing PUT endpoint. | `19712d8` (C1) · `d5c6f12` (C2) · (current C3) |
|
||||||
| 2026-05-07 | Claude | **Migration 17 — manual budget fields fallback cho PE + HĐ (toggle "Nhập tay")** — User feedback: khi project chưa có Budget approved (Phase=DaDuyet eligible), user phải break flow đi tạo Budget + duyệt + quay lại link. UX kém. Solution: thêm fallback "Nhập tay" — checkbox toggle cạnh Label Ngân sách, khi ON → hide Select Budget, show 2 input field grid 2-col (Tên tham chiếu text + Số tiền number formatted VND). Lưu trên entity row, KHÔNG cần Budget entity. Q1-3 chốt: 1 = stick-toggle reveal 2 input fields; 2 = cả BudgetId + manual fields cùng null OK (KHÔNG XOR validate); 3 = mirror logic sang HĐ luôn (cả 7 ContractType qua ContractCreatePage). 5 chunk per-commit (build + 83 test pass mỗi chunk): C1 Migration 17 `AddManualBudgetFieldsToPeAndContract` 4 ALTER (PE + HĐ × Name nvarchar(200) + Amount decimal(18,2)) + Domain 2 entity + 2 EF config (HasMaxLength + HasPrecision) — applied LocalDB OK, 3-file rule. C2 App CQRS — CreatePurchaseEvaluationCommand + Update + DTO + Validator (>=0 when has value), CreateContractCommand + Update + DTO + diff log audit, CreateContractFromEvaluation carry forward pe.BudgetManualName/Amount → contract khi gen HĐ từ phiếu. C3 FE-Admin — types +2 field, PeHeaderForm toggle + 2 input + payload conditional (manual mode clear budgetId, link mode clear manual), PeDetailTabs Section "b. Ngân sách" fallback display "Tên · Số tiền + badge nhập tay" khi !budget + có manual data, refactor PurchaseEvaluationCreatePage wrap PeHeaderForm DRY (222→30 LOC), ContractCreatePage NewContractForm + EditContractForm cùng pattern + read-only display branch khi !isDraft. C4 fe-user mirror y hệt 6 file. C5 docs (this row + HANDOFF + session log). KHÔNG đụng Budget entity / Phase=DaDuyet validation (giữ invariant). | `ecd5f7e` (C1) · `0f7901c` (C2) · `bab5031` (C3) · `14f8d9d` (C4) · (current C5) |
|
| 2026-05-07 | Claude | **Migration 17 — manual budget fields fallback cho PE + HĐ (toggle "Nhập tay")** — User feedback: khi project chưa có Budget approved (Phase=DaDuyet eligible), user phải break flow đi tạo Budget + duyệt + quay lại link. UX kém. Solution: thêm fallback "Nhập tay" — checkbox toggle cạnh Label Ngân sách, khi ON → hide Select Budget, show 2 input field grid 2-col (Tên tham chiếu text + Số tiền number formatted VND). Lưu trên entity row, KHÔNG cần Budget entity. Q1-3 chốt: 1 = stick-toggle reveal 2 input fields; 2 = cả BudgetId + manual fields cùng null OK (KHÔNG XOR validate); 3 = mirror logic sang HĐ luôn (cả 7 ContractType qua ContractCreatePage). 5 chunk per-commit (build + 83 test pass mỗi chunk): C1 Migration 17 `AddManualBudgetFieldsToPeAndContract` 4 ALTER (PE + HĐ × Name nvarchar(200) + Amount decimal(18,2)) + Domain 2 entity + 2 EF config (HasMaxLength + HasPrecision) — applied LocalDB OK, 3-file rule. C2 App CQRS — CreatePurchaseEvaluationCommand + Update + DTO + Validator (>=0 when has value), CreateContractCommand + Update + DTO + diff log audit, CreateContractFromEvaluation carry forward pe.BudgetManualName/Amount → contract khi gen HĐ từ phiếu. C3 FE-Admin — types +2 field, PeHeaderForm toggle + 2 input + payload conditional (manual mode clear budgetId, link mode clear manual), PeDetailTabs Section "b. Ngân sách" fallback display "Tên · Số tiền + badge nhập tay" khi !budget + có manual data, refactor PurchaseEvaluationCreatePage wrap PeHeaderForm DRY (222→30 LOC), ContractCreatePage NewContractForm + EditContractForm cùng pattern + read-only display branch khi !isDraft. C4 fe-user mirror y hệt 6 file. C5 docs (this row + HANDOFF + session log). KHÔNG đụng Budget entity / Phase=DaDuyet validation (giữ invariant). | `ecd5f7e` (C1) · `0f7901c` (C2) · `bab5031` (C3) · `14f8d9d` (C4) · (current C5) |
|
||||||
| 2026-05-07 | Claude | **PE "Thao tác" 2-panel workspace + Panel 1 read-only picker + Section 5 disabled** — User chỉ thị restructure leaf "Thao tác" (Pe_DuyetNcc_Create + Pe_DuyetNccPhuongAn_Create) từ page tạo header riêng (`/purchase-evaluations/new?type=N` — chỉ form Tên/Project/Địa điểm/Payment/Budget) sang workspace 2-panel mirror pattern HĐ Thầu phụ ContractCreatePage. 5 câu chốt spec trước code: Q1 Panel 2 KHÔNG render Workflow Panel + Approvals + History (chỉ data entry); Panel 1 = pure picker, KHÔNG inline edit/delete; Q2 mirror HĐ Thầu phụ pattern (sticky "+ Thêm mới" + Panel 2 transition new→edit form); Q3 leaf "Danh sách" + "Duyệt" giữ 3-panel hiện tại; Q4 route mới `/purchase-evaluations/workspace?type={1\|2}`; Q5 Section 5 Ý kiến 4PB disable trong workspace (vì người ta nhập khi duyệt, không phải lúc nhập liệu). 2 chunk per-commit (build + 83 test pass mỗi chunk): C1 fe-admin (3 file mới `PeListPanel.tsx` ~180 LOC pure picker reuse + `PeHeaderForm.tsx` ~210 LOC extract + `PurchaseEvaluationWorkspacePage.tsx` ~120 LOC, 3 file sửa `PeDetailTabs.tsx` thêm prop `mode?: 'detail' \| 'workspace'` + Section 5 hint banner amber + Layout.tsx resolver `Pe_*_Create`→`/workspace?type=N` + App.tsx route mới). C2 fe-user mirror y hệt 6 file (rule §3.9). KHÔNG đụng BE / migration / schema / endpoint. Route `/new` cũ giữ tồn tại cho deep-link "Sửa header" button. **Total +1142 LOC FE / 0 BE / 32 FE pages.** | `ee0d360` (C1) · `ecf3c59` (C2) · (current C3) |
|
| 2026-05-07 | Claude | **PE "Thao tác" 2-panel workspace + Panel 1 read-only picker + Section 5 disabled** — User chỉ thị restructure leaf "Thao tác" (Pe_DuyetNcc_Create + Pe_DuyetNccPhuongAn_Create) từ page tạo header riêng (`/purchase-evaluations/new?type=N` — chỉ form Tên/Project/Địa điểm/Payment/Budget) sang workspace 2-panel mirror pattern HĐ Thầu phụ ContractCreatePage. 5 câu chốt spec trước code: Q1 Panel 2 KHÔNG render Workflow Panel + Approvals + History (chỉ data entry); Panel 1 = pure picker, KHÔNG inline edit/delete; Q2 mirror HĐ Thầu phụ pattern (sticky "+ Thêm mới" + Panel 2 transition new→edit form); Q3 leaf "Danh sách" + "Duyệt" giữ 3-panel hiện tại; Q4 route mới `/purchase-evaluations/workspace?type={1\|2}`; Q5 Section 5 Ý kiến 4PB disable trong workspace (vì người ta nhập khi duyệt, không phải lúc nhập liệu). 2 chunk per-commit (build + 83 test pass mỗi chunk): C1 fe-admin (3 file mới `PeListPanel.tsx` ~180 LOC pure picker reuse + `PeHeaderForm.tsx` ~210 LOC extract + `PurchaseEvaluationWorkspacePage.tsx` ~120 LOC, 3 file sửa `PeDetailTabs.tsx` thêm prop `mode?: 'detail' \| 'workspace'` + Section 5 hint banner amber + Layout.tsx resolver `Pe_*_Create`→`/workspace?type=N` + App.tsx route mới). C2 fe-user mirror y hệt 6 file (rule §3.9). KHÔNG đụng BE / migration / schema / endpoint. Route `/new` cũ giữ tồn tại cho deep-link "Sửa header" button. **Total +1142 LOC FE / 0 BE / 32 FE pages.** | `ee0d360` (C1) · `ecf3c59` (C2) · (current C3) |
|
||||||
|
|||||||
@ -53,6 +53,7 @@ export function PeDetailTabs({
|
|||||||
onDelete,
|
onDelete,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
mode = 'detail',
|
mode = 'detail',
|
||||||
|
autoEditHeader = false,
|
||||||
}: {
|
}: {
|
||||||
evaluation: PeDetailBundle
|
evaluation: PeDetailBundle
|
||||||
onBack: () => void
|
onBack: () => void
|
||||||
@ -61,6 +62,8 @@ export function PeDetailTabs({
|
|||||||
readOnly?: boolean
|
readOnly?: boolean
|
||||||
/** 'workspace' = Section 5 LUÔN disabled (ý kiến nhập ở leaf Duyệt). */
|
/** 'workspace' = Section 5 LUÔN disabled (ý kiến nhập ở leaf Duyệt). */
|
||||||
mode?: 'detail' | 'workspace'
|
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 navigate = useNavigate()
|
||||||
const isDraft = evaluation.phase === PurchaseEvaluationPhase.DangSoanThao
|
const isDraft = evaluation.phase === PurchaseEvaluationPhase.DangSoanThao
|
||||||
@ -113,7 +116,7 @@ export function PeDetailTabs({
|
|||||||
<div className="divide-y divide-slate-200">
|
<div className="divide-y divide-slate-200">
|
||||||
{/* Section 1 — đúng spec form FO-PHIẾU TRÌNH KÝ CHỌN TP/NCC */}
|
{/* 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">
|
<Section title="1. Thông tin gói thầu">
|
||||||
<InfoTab ev={evaluation} />
|
<InfoTab ev={evaluation} readOnly={readOnly} autoEdit={autoEditHeader} />
|
||||||
</Section>
|
</Section>
|
||||||
<Section title="2. Chọn NCC / TP">
|
<Section title="2. Chọn NCC / TP">
|
||||||
<ChonNccSection ev={evaluation} readOnly={readOnly} />
|
<ChonNccSection ev={evaluation} readOnly={readOnly} />
|
||||||
@ -292,19 +295,141 @@ 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) =====
|
// ===== 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 }) {
|
// 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 (
|
return (
|
||||||
<dl className="space-y-2 text-sm">
|
<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} />
|
<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>
|
||||||
<FormRow label="b. Dự án" value={ev.projectName} />
|
<FormRow label="b. Dự án" value={ev.projectName} />
|
||||||
{(ev.diaDiem || ev.moTa) && (
|
{(ev.diaDiem || ev.moTa || ev.paymentTerms) && (
|
||||||
<div className="mt-3 rounded bg-slate-50 px-3 py-2 text-[12px] text-slate-600">
|
<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.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.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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</dl>
|
</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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== b. Ngân sách inline editor (Mig 17) =====
|
// ===== b. Ngân sách inline editor (Mig 17) =====
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
// chỉ render + invoke callbacks. Pendingme vẫn truyền được nếu cần dùng cho
|
// 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).
|
// inbox view khác (hiện chỉ workspace dùng pendingMe=false).
|
||||||
import { useQuery } from '@tanstack/react-query'
|
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 { Button } from '@/components/ui/Button'
|
||||||
import { Input } from '@/components/ui/Input'
|
import { Input } from '@/components/ui/Input'
|
||||||
import { Select } from '@/components/ui/Select'
|
import { Select } from '@/components/ui/Select'
|
||||||
@ -34,6 +34,7 @@ export function PeListPanel({
|
|||||||
onPhaseChange,
|
onPhaseChange,
|
||||||
showCreateButton = false,
|
showCreateButton = false,
|
||||||
onCreate,
|
onCreate,
|
||||||
|
onEditClick,
|
||||||
}: {
|
}: {
|
||||||
typeFilter: number | null
|
typeFilter: number | null
|
||||||
pendingMe?: boolean
|
pendingMe?: boolean
|
||||||
@ -45,6 +46,8 @@ export function PeListPanel({
|
|||||||
onPhaseChange: (p: string) => void
|
onPhaseChange: (p: string) => void
|
||||||
showCreateButton?: boolean
|
showCreateButton?: boolean
|
||||||
onCreate?: () => void
|
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({
|
const list = useQuery({
|
||||||
queryKey: ['pe-list', { typeFilter, pendingMe, search, phase }],
|
queryKey: ['pe-list', { typeFilter, pendingMe, search, phase }],
|
||||||
@ -122,11 +125,11 @@ export function PeListPanel({
|
|||||||
)}
|
)}
|
||||||
<ul className="divide-y divide-slate-100">
|
<ul className="divide-y divide-slate-100">
|
||||||
{rows.map(p => (
|
{rows.map(p => (
|
||||||
<li key={p.id}>
|
<li key={p.id} className="group relative">
|
||||||
<button
|
<button
|
||||||
onClick={() => onSelect(p.id)}
|
onClick={() => onSelect(p.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'block w-full px-3 py-2.5 text-left transition hover:bg-slate-50',
|
'block w-full px-3 py-2.5 pr-9 text-left transition hover:bg-slate-50',
|
||||||
selectedId === p.id && 'bg-brand-50 ring-1 ring-inset ring-brand-200',
|
selectedId === p.id && 'bg-brand-50 ring-1 ring-inset ring-brand-200',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -163,6 +166,16 @@ export function PeListPanel({
|
|||||||
<div className="mt-1 text-[10px] text-brand-600">✓ Đã tạo HĐ</div>
|
<div className="mt-1 text-[10px] text-brand-600">✓ Đã tạo HĐ</div>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
{/* Edit pencil — visible on hover (chỉ khi onEditClick được truyền) */}
|
||||||
|
{onEditClick && (
|
||||||
|
<button
|
||||||
|
onClick={() => onEditClick(p.id)}
|
||||||
|
className="absolute right-2 top-2 rounded p-1.5 text-slate-400 opacity-0 transition group-hover:opacity-100 hover:bg-white hover:text-brand-600 hover:shadow-sm"
|
||||||
|
title="Sửa thông tin gói thầu"
|
||||||
|
>
|
||||||
|
<Pencil className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -34,6 +34,7 @@ export function PurchaseEvaluationWorkspacePage() {
|
|||||||
const phase = sp.get('phase') ?? ''
|
const phase = sp.get('phase') ?? ''
|
||||||
const selectedId = sp.get('id')
|
const selectedId = sp.get('id')
|
||||||
const mode = sp.get('mode') // 'new' | null
|
const mode = sp.get('mode') // 'new' | null
|
||||||
|
const autoEditHeader = sp.get('editHeader') === '1'
|
||||||
|
|
||||||
const detail = useQuery({
|
const detail = useQuery({
|
||||||
queryKey: ['pe-detail', selectedId],
|
queryKey: ['pe-detail', selectedId],
|
||||||
@ -77,17 +78,18 @@ export function PurchaseEvaluationWorkspacePage() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[320px_1fr]">
|
<div className="grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[320px_1fr]">
|
||||||
{/* Panel 1: List pure picker + sticky create */}
|
{/* Panel 1: List pure picker + sticky create + pencil edit hover */}
|
||||||
<PeListPanel
|
<PeListPanel
|
||||||
typeFilter={typeFilter}
|
typeFilter={typeFilter}
|
||||||
selectedId={selectedId}
|
selectedId={selectedId}
|
||||||
search={search}
|
search={search}
|
||||||
phase={phase}
|
phase={phase}
|
||||||
onSelect={id => setParams({ id, mode: null })}
|
onSelect={id => setParams({ id, mode: null, editHeader: null })}
|
||||||
onSearchChange={q => setParams({ q })}
|
onSearchChange={q => setParams({ q })}
|
||||||
onPhaseChange={p => setParams({ phase: p })}
|
onPhaseChange={p => setParams({ phase: p })}
|
||||||
showCreateButton
|
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) */}
|
{/* Panel 2: Empty | Header form | Detail tabs (workspace mode) */}
|
||||||
@ -117,9 +119,10 @@ export function PurchaseEvaluationWorkspacePage() {
|
|||||||
{selectedId && detail.data && (
|
{selectedId && detail.data && (
|
||||||
<PeDetailTabs
|
<PeDetailTabs
|
||||||
evaluation={detail.data}
|
evaluation={detail.data}
|
||||||
onBack={() => setParams({ id: null })}
|
onBack={() => setParams({ id: null, editHeader: null })}
|
||||||
onDelete={() => del.mutate(detail.data!.id)}
|
onDelete={() => del.mutate(detail.data!.id)}
|
||||||
mode="workspace"
|
mode="workspace"
|
||||||
|
autoEditHeader={autoEditHeader}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@ -53,6 +53,7 @@ export function PeDetailTabs({
|
|||||||
onDelete,
|
onDelete,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
mode = 'detail',
|
mode = 'detail',
|
||||||
|
autoEditHeader = false,
|
||||||
}: {
|
}: {
|
||||||
evaluation: PeDetailBundle
|
evaluation: PeDetailBundle
|
||||||
onBack: () => void
|
onBack: () => void
|
||||||
@ -61,6 +62,8 @@ export function PeDetailTabs({
|
|||||||
readOnly?: boolean
|
readOnly?: boolean
|
||||||
/** 'workspace' = Section 5 LUÔN disabled (ý kiến nhập ở leaf Duyệt). */
|
/** 'workspace' = Section 5 LUÔN disabled (ý kiến nhập ở leaf Duyệt). */
|
||||||
mode?: 'detail' | 'workspace'
|
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 navigate = useNavigate()
|
||||||
const isDraft = evaluation.phase === PurchaseEvaluationPhase.DangSoanThao
|
const isDraft = evaluation.phase === PurchaseEvaluationPhase.DangSoanThao
|
||||||
@ -113,7 +116,7 @@ export function PeDetailTabs({
|
|||||||
<div className="divide-y divide-slate-200">
|
<div className="divide-y divide-slate-200">
|
||||||
{/* Section 1 — đúng spec form FO-PHIẾU TRÌNH KÝ CHỌN TP/NCC */}
|
{/* 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">
|
<Section title="1. Thông tin gói thầu">
|
||||||
<InfoTab ev={evaluation} />
|
<InfoTab ev={evaluation} readOnly={readOnly} autoEdit={autoEditHeader} />
|
||||||
</Section>
|
</Section>
|
||||||
<Section title="2. Chọn NCC / TP">
|
<Section title="2. Chọn NCC / TP">
|
||||||
<ChonNccSection ev={evaluation} readOnly={readOnly} />
|
<ChonNccSection ev={evaluation} readOnly={readOnly} />
|
||||||
@ -292,19 +295,141 @@ 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) =====
|
// ===== 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 }) {
|
// 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 (
|
return (
|
||||||
<dl className="space-y-2 text-sm">
|
<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} />
|
<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>
|
||||||
<FormRow label="b. Dự án" value={ev.projectName} />
|
<FormRow label="b. Dự án" value={ev.projectName} />
|
||||||
{(ev.diaDiem || ev.moTa) && (
|
{(ev.diaDiem || ev.moTa || ev.paymentTerms) && (
|
||||||
<div className="mt-3 rounded bg-slate-50 px-3 py-2 text-[12px] text-slate-600">
|
<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.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.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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</dl>
|
</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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== b. Ngân sách inline editor (Mig 17) =====
|
// ===== b. Ngân sách inline editor (Mig 17) =====
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
// chỉ render + invoke callbacks. Pendingme vẫn truyền được nếu cần dùng cho
|
// 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).
|
// inbox view khác (hiện chỉ workspace dùng pendingMe=false).
|
||||||
import { useQuery } from '@tanstack/react-query'
|
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 { Button } from '@/components/ui/Button'
|
||||||
import { Input } from '@/components/ui/Input'
|
import { Input } from '@/components/ui/Input'
|
||||||
import { Select } from '@/components/ui/Select'
|
import { Select } from '@/components/ui/Select'
|
||||||
@ -34,6 +34,7 @@ export function PeListPanel({
|
|||||||
onPhaseChange,
|
onPhaseChange,
|
||||||
showCreateButton = false,
|
showCreateButton = false,
|
||||||
onCreate,
|
onCreate,
|
||||||
|
onEditClick,
|
||||||
}: {
|
}: {
|
||||||
typeFilter: number | null
|
typeFilter: number | null
|
||||||
pendingMe?: boolean
|
pendingMe?: boolean
|
||||||
@ -45,6 +46,8 @@ export function PeListPanel({
|
|||||||
onPhaseChange: (p: string) => void
|
onPhaseChange: (p: string) => void
|
||||||
showCreateButton?: boolean
|
showCreateButton?: boolean
|
||||||
onCreate?: () => void
|
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({
|
const list = useQuery({
|
||||||
queryKey: ['pe-list', { typeFilter, pendingMe, search, phase }],
|
queryKey: ['pe-list', { typeFilter, pendingMe, search, phase }],
|
||||||
@ -122,11 +125,11 @@ export function PeListPanel({
|
|||||||
)}
|
)}
|
||||||
<ul className="divide-y divide-slate-100">
|
<ul className="divide-y divide-slate-100">
|
||||||
{rows.map(p => (
|
{rows.map(p => (
|
||||||
<li key={p.id}>
|
<li key={p.id} className="group relative">
|
||||||
<button
|
<button
|
||||||
onClick={() => onSelect(p.id)}
|
onClick={() => onSelect(p.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'block w-full px-3 py-2.5 text-left transition hover:bg-slate-50',
|
'block w-full px-3 py-2.5 pr-9 text-left transition hover:bg-slate-50',
|
||||||
selectedId === p.id && 'bg-brand-50 ring-1 ring-inset ring-brand-200',
|
selectedId === p.id && 'bg-brand-50 ring-1 ring-inset ring-brand-200',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -163,6 +166,16 @@ export function PeListPanel({
|
|||||||
<div className="mt-1 text-[10px] text-brand-600">✓ Đã tạo HĐ</div>
|
<div className="mt-1 text-[10px] text-brand-600">✓ Đã tạo HĐ</div>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
{/* Edit pencil — visible on hover (chỉ khi onEditClick được truyền) */}
|
||||||
|
{onEditClick && (
|
||||||
|
<button
|
||||||
|
onClick={() => onEditClick(p.id)}
|
||||||
|
className="absolute right-2 top-2 rounded p-1.5 text-slate-400 opacity-0 transition group-hover:opacity-100 hover:bg-white hover:text-brand-600 hover:shadow-sm"
|
||||||
|
title="Sửa thông tin gói thầu"
|
||||||
|
>
|
||||||
|
<Pencil className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -34,6 +34,7 @@ export function PurchaseEvaluationWorkspacePage() {
|
|||||||
const phase = sp.get('phase') ?? ''
|
const phase = sp.get('phase') ?? ''
|
||||||
const selectedId = sp.get('id')
|
const selectedId = sp.get('id')
|
||||||
const mode = sp.get('mode') // 'new' | null
|
const mode = sp.get('mode') // 'new' | null
|
||||||
|
const autoEditHeader = sp.get('editHeader') === '1'
|
||||||
|
|
||||||
const detail = useQuery({
|
const detail = useQuery({
|
||||||
queryKey: ['pe-detail', selectedId],
|
queryKey: ['pe-detail', selectedId],
|
||||||
@ -77,17 +78,18 @@ export function PurchaseEvaluationWorkspacePage() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[320px_1fr]">
|
<div className="grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[320px_1fr]">
|
||||||
{/* Panel 1: List pure picker + sticky create */}
|
{/* Panel 1: List pure picker + sticky create + pencil edit hover */}
|
||||||
<PeListPanel
|
<PeListPanel
|
||||||
typeFilter={typeFilter}
|
typeFilter={typeFilter}
|
||||||
selectedId={selectedId}
|
selectedId={selectedId}
|
||||||
search={search}
|
search={search}
|
||||||
phase={phase}
|
phase={phase}
|
||||||
onSelect={id => setParams({ id, mode: null })}
|
onSelect={id => setParams({ id, mode: null, editHeader: null })}
|
||||||
onSearchChange={q => setParams({ q })}
|
onSearchChange={q => setParams({ q })}
|
||||||
onPhaseChange={p => setParams({ phase: p })}
|
onPhaseChange={p => setParams({ phase: p })}
|
||||||
showCreateButton
|
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) */}
|
{/* Panel 2: Empty | Header form | Detail tabs (workspace mode) */}
|
||||||
@ -117,9 +119,10 @@ export function PurchaseEvaluationWorkspacePage() {
|
|||||||
{selectedId && detail.data && (
|
{selectedId && detail.data && (
|
||||||
<PeDetailTabs
|
<PeDetailTabs
|
||||||
evaluation={detail.data}
|
evaluation={detail.data}
|
||||||
onBack={() => setParams({ id: null })}
|
onBack={() => setParams({ id: null, editHeader: null })}
|
||||||
onDelete={() => del.mutate(detail.data!.id)}
|
onDelete={() => del.mutate(detail.data!.id)}
|
||||||
mode="workspace"
|
mode="workspace"
|
||||||
|
autoEditHeader={autoEditHeader}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in New Issue
Block a user