diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index e7ecbb9..9277017 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/src/components/pe/PeDetailTabs.tsx @@ -158,6 +158,18 @@ function InfoTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: bool + + {ev.budget.maNganSach ?? '—'} + {' · '} + {ev.budget.tenNganSach} + {' · '} + {ev.budget.tongNganSach.toLocaleString('vi-VN')} đ + + ) : — (chưa link)} + /> {ev.contractId && ( ✓ Xem HĐ} /> )} @@ -471,6 +483,25 @@ function ItemsTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boo const quoteKey = (detailId: string, supplierRowId: string) => ev.details.find(d => d.id === detailId)?.quotes.find(q => q.purchaseEvaluationSupplierId === supplierRowId) ?? null + // Budget comparison — fetch full Budget bundle nếu có link để so sánh per-row. + // Match key: groupCode|itemCode (case-sensitive match; itemCode null cho phép). + const budgetBundle = useQuery({ + queryKey: ['budget-detail-for-pe', ev.budgetId], + queryFn: async () => (await api.get<{ details: { groupCode: string; itemCode: string | null; thanhTien: number }[]; tongNganSach: number }>( + `/budgets/${ev.budgetId}`)).data, + enabled: !!ev.budgetId, + }) + const budgetRowMap = (() => { + const m = new Map() + budgetBundle.data?.details.forEach(d => { + m.set(`${d.groupCode}|${d.itemCode ?? ''}`, d.thanhTien) + }) + return m + })() + const showBudgetCol = !!ev.budgetId + const totalPeNganSach = ev.details.reduce((sum, d) => sum + d.thanhTienNganSach, 0) + const totalBudget = budgetBundle.data?.tongNganSach ?? 0 + return (
@@ -499,6 +530,11 @@ function ItemsTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boo KL ĐG ngân sách TT ngân sách + {showBudgetCol && ( + + NS link · Δ + + )} {ev.suppliers.map(s => ( {s.displayName ?? s.supplierName} @@ -517,6 +553,26 @@ function ItemsTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boo {d.khoiLuongNganSach} {fmtMoney(d.donGiaNganSach)} {fmtMoney(d.thanhTienNganSach)} + {showBudgetCol && (() => { + const bgValue = budgetRowMap.get(`${d.groupCode}|${d.itemCode ?? ''}`) + if (bgValue == null) + return — + const delta = d.thanhTienNganSach - bgValue + return ( + 0 && 'text-red-600', + delta < 0 && 'text-emerald-600', + delta === 0 && 'text-slate-500', + )} + title={`Ngân sách: ${fmtMoney(bgValue)} · Δ ${delta > 0 ? '+' : ''}${fmtMoney(delta)}`} + > + {fmtMoney(bgValue)} +
{delta === 0 ? '=' : (delta > 0 ? `+${fmtMoney(delta)}` : fmtMoney(delta))}
+ + ) + })()} {ev.suppliers.map(s => { const q = quoteKey(d.id, s.id) return ( @@ -548,6 +604,34 @@ function ItemsTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boo ))} + {showBudgetCol && ( + + + Tổng: + + + {fmtMoney(totalPeNganSach)} + + {fmtMoney(totalBudget)} + {(() => { + const delta = totalPeNganSach - totalBudget + return ( +
0 && 'text-red-600', + delta < 0 && 'text-emerald-600', + delta === 0 && 'text-slate-500', + )}> + {delta === 0 ? 'khớp ngân sách' : delta > 0 ? `vượt +${fmtMoney(delta)}` : `dưới ${fmtMoney(delta)}`} +
+ ) + })()} + + {ev.suppliers.map(s => )} + {!readOnly && } + + + )}
)} diff --git a/fe-admin/src/pages/contracts/ContractCreatePage.tsx b/fe-admin/src/pages/contracts/ContractCreatePage.tsx index afdf556..6d3d889 100644 --- a/fe-admin/src/pages/contracts/ContractCreatePage.tsx +++ b/fe-admin/src/pages/contracts/ContractCreatePage.tsx @@ -35,6 +35,7 @@ import { type ContractDetail, type ContractListItem, } from '@/types/contracts' +import { BudgetPhase, type BudgetListItem } from '@/types/budget' const fmtMoney = (v: number) => v.toLocaleString('vi-VN') @@ -303,9 +304,12 @@ function ContractHeaderForm({ const [tenHopDong, setTenHopDong] = useState('') const [noiDung, setNoiDung] = useState('') const [bypass, setBypass] = useState(false) + const [budgetId, setBudgetId] = useState('') // Reset type về default khi typeFilter (parent prop) thay đổi useEffect(() => { setType(defaultType) }, [defaultType]) + // Reset budget khi đổi project (mỗi project có ngân sách riêng) + useEffect(() => { setBudgetId('') }, [projectId]) const suppliers = useQuery({ queryKey: ['suppliers-all'], @@ -319,6 +323,15 @@ function ContractHeaderForm({ queryKey: ['templates-by-type', type], queryFn: async () => (await api.get('/forms/templates', { params: { type } })).data, }) + // Eligible Budgets: cùng Project + Phase=DaDuyet (BE-side filter). + const eligibleBudgets = useQuery({ + queryKey: ['eligible-budgets', projectId], + queryFn: async () => + (await api.get>('/budgets', { + params: { pageSize: 100, projectId, phase: BudgetPhase.DaDuyet }, + })).data.items, + enabled: !!projectId, + }) const qc = useQueryClient() const create = useMutation({ @@ -334,6 +347,7 @@ function ContractHeaderForm({ noiDung: noiDung || null, bypassProcurementAndCCM: bypass, draftData: null, + budgetId: budgetId || null, }) return res.data.id }, @@ -372,6 +386,28 @@ function ContractHeaderForm({ templates={templates.data ?? []} typeReadonly={false} /> +
+ + +

+ {!projectId + ? 'Chọn dự án trước để xem ngân sách khả dụng.' + : eligibleBudgets.data && eligibleBudgets.data.length === 0 + ? 'Dự án này chưa có ngân sách đã duyệt.' + : 'Chỉ list ngân sách đã duyệt cùng dự án.'} +

+