diff --git a/fe-user/src/components/pe/PeDetailTabs.tsx b/fe-user/src/components/pe/PeDetailTabs.tsx index e23ad51..233c7ce 100644 --- a/fe-user/src/components/pe/PeDetailTabs.tsx +++ b/fe-user/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)} - /> +