From 19712d89fc7e6e3d3357f62efd3db29c6ce9f022 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Thu, 7 May 2026 13:12:20 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20FE-Admin:=20PE=20BudgetFieldRow=20in?= =?UTF-8?q?line=20editor=20=E2=80=94=20toggle=20+=202=20fields=20trong=20S?= =?UTF-8?q?ection=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User feedback 2026-05-07: muốn toggle "Nhập tay" + 2 input fields hiển thị trực tiếp trong Section 2 "b. Ngân sách" (PeDetailTabs) — KHÔNG cần đi tới "Sửa header" page. Visible trong cả 3 view (Workspace / Danh sách / Duyệt). Empty giá trị thì hiển thị empty. Implementation: + BudgetFieldRow component (~125 LOC) thay cho FormRow tĩnh cũ - Detect mode auto khi mount: prefer manual mode nếu ev.budgetManualName/Amount set + !ev.budgetId - canEdit = !readOnly && isDraft (DangSoanThao): → Render toggle "Nhập tay" + 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" reset → Save: full PUT /pe/:id với current values (tenGoiThau/diaDiem/moTa/ paymentTerms) + new budget payload conditional (manual mode → clear budgetId, link mode → clear manual). Invalidate ['pe-detail', 'pe-list']. - canEdit=false (Duyệt mode hoặc !isDraft): → Display only — link card / manual values / empty "—" (không text "chưa link" verbose nữa per user "giá trị rỗng thì cứ hiển thị rỗng") Files: ~ fe-admin/src/components/pe/PeDetailTabs.tsx - import BudgetPhase + BudgetListItem từ types/budget + Paged từ types/master - new BudgetFieldRow component - ChonNccSection b. Ngân sách FormRow → (1-line replacement) Verify: npm run build fe-admin pass · 1922 modules · 0 TS error. Next: Chunk 2 fe-user mirror. Co-Authored-By: Claude Opus 4.7 (1M context) --- fe-admin/src/components/pe/PeDetailTabs.tsx | 177 +++++++++++++++++--- 1 file changed, 155 insertions(+), 22 deletions(-) diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index e23ad51..233c7ce 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/src/components/pe/PeDetailTabs.tsx @@ -32,7 +32,8 @@ import { type PeQuote, type PeSupplier, } from '@/types/purchaseEvaluation' -import type { Supplier } from '@/types/master' +import { BudgetPhase, type BudgetListItem } from '@/types/budget' +import type { Paged, Supplier } from '@/types/master' const fmtMoney = (v: number) => v.toLocaleString('vi-VN') @@ -306,6 +307,158 @@ function InfoTab({ ev }: { ev: PeDetailBundle }) { ) } +// ===== b. Ngân sách inline editor (Mig 17) ===== +// Hiển thị + edit budget link / manual fields ngay trong Section 2 — KHÔNG cần +// đi tới "Sửa header" page. Visible trong cả 3 view (Workspace / Danh sách / +// Duyệt). Edit chỉ enable khi !readOnly + isDraft (Drafter sửa). Read-only +// khi pendingMe=1 hoặc phase đã chuyển khỏi DangSoanThao. Empty values hiển +// thị empty (per user 2026-05-07). +function BudgetFieldRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolean }) { + const isDraft = ev.phase === PurchaseEvaluationPhase.DangSoanThao + const canEdit = !readOnly && isDraft + const qc = useQueryClient() + + // Detect mode khi mount/refresh: prefer manual mode nếu đã có data manual + ko link + const initialManual = (ev.budgetManualName !== null || ev.budgetManualAmount !== null) && !ev.budgetId + const [manualMode, setManualMode] = useState(initialManual) + const [budgetId, setBudgetId] = useState(ev.budgetId ?? '') + const [manualName, setManualName] = useState(ev.budgetManualName ?? '') + const [manualAmount, setManualAmount] = useState(ev.budgetManualAmount ?? 0) + + // Eligible budgets — chỉ fetch khi user có khả năng edit + const eligibleBudgets = useQuery({ + queryKey: ['eligible-budgets', ev.projectId], + queryFn: async () => (await api.get>('/budgets', { + params: { pageSize: 100, projectId: ev.projectId, phase: BudgetPhase.DaDuyet }, + })).data.items, + enabled: canEdit, + }) + + // Dirty detect — compare current state vs ev original + const dirty = manualMode !== initialManual + || (manualMode && (manualName !== (ev.budgetManualName ?? '') || manualAmount !== (ev.budgetManualAmount ?? 0))) + || (!manualMode && budgetId !== (ev.budgetId ?? '')) + + const save = useMutation({ + mutationFn: async () => { + const payload = manualMode + ? { budgetId: null, budgetManualName: manualName || null, budgetManualAmount: manualAmount > 0 ? manualAmount : null } + : { budgetId: budgetId || null, budgetManualName: null, budgetManualAmount: null } + await api.put(`/purchase-evaluations/${ev.id}`, { + id: ev.id, + tenGoiThau: ev.tenGoiThau, + diaDiem: ev.diaDiem, + moTa: ev.moTa, + paymentTerms: ev.paymentTerms, + ...payload, + }) + }, + onSuccess: () => { + toast.success('Đã cập nhật ngân sách') + qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] }) + qc.invalidateQueries({ queryKey: ['pe-list'] }) + }, + onError: e => toast.error(getErrorMessage(e)), + }) + + // Read-only mode: chỉ display (không toggle, không edit) + if (!canEdit) { + return ( + + {ev.budget.maNganSach ?? '—'} + {' · '}{ev.budget.tenNganSach} + {' · '}{ev.budget.tongNganSach.toLocaleString('vi-VN')} đ + + ) : ev.budgetManualAmount != null || ev.budgetManualName ? ( + + {ev.budgetManualName && {ev.budgetManualName}} + {ev.budgetManualName && ev.budgetManualAmount != null && ' · '} + {ev.budgetManualAmount != null && ( + {ev.budgetManualAmount.toLocaleString('vi-VN')} đ + )} + nhập tay + + ) : } + /> + ) + } + + // Editable mode (canEdit=true) + return ( +
+ b. Ngân sách +
+ + {!manualMode ? ( + + ) : ( +
+ setManualName(e.target.value)} + placeholder="Tên ngân sách (vd Tạm tính T11/2025)" + maxLength={200} + className="text-sm" + /> + setManualAmount(Number(e.target.value))} + placeholder="Số tiền (đ)" + className="text-sm" + /> +
+ )} + {dirty && ( +
+ + +
+ )} +
+
+ ) +} + // ===== Section 2 — Chọn NCC/TP (spec: a/b/c/d) ===== function ChonNccSection({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boolean }) { const canCreateContract = !readOnly && ev.phase === PurchaseEvaluationPhase.DaDuyet && !ev.contractId && ev.selectedSupplierId @@ -335,27 +488,7 @@ function ChonNccSection({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly ✓ {ev.selectedSupplierName} ) : — (chưa chọn)} /> - - {ev.budget.maNganSach ?? '—'} - {' · '}{ev.budget.tenNganSach} - {' · '}{ev.budget.tongNganSach.toLocaleString('vi-VN')} đ - - ) : ev.budgetManualAmount != null || ev.budgetManualName ? ( - // Mig 17 — manual budget fallback: hiển thị tên + số tiền nhập tay, - // không phải link vào /budgets/{id} (không có Budget entity). - - {ev.budgetManualName && {ev.budgetManualName}} - {ev.budgetManualName && ev.budgetManualAmount != null && ' · '} - {ev.budgetManualAmount != null && ( - {ev.budgetManualAmount.toLocaleString('vi-VN')} đ - )} - nhập tay - - ) : — (chưa link)} - /> +