diff --git a/fe-user/src/components/pe/PeDetailTabs.tsx b/fe-user/src/components/pe/PeDetailTabs.tsx index 1e81ed9..e23ad51 100644 --- a/fe-user/src/components/pe/PeDetailTabs.tsx +++ b/fe-user/src/components/pe/PeDetailTabs.tsx @@ -343,6 +343,17 @@ function ChonNccSection({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly {' · '}{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)} /> { if (existing.data) { + const hasManual = existing.data.budgetManualName !== null + || existing.data.budgetManualAmount !== null setForm({ type: existing.data.type, tenGoiThau: existing.data.tenGoiThau, @@ -76,10 +82,27 @@ export function PeHeaderForm({ moTa: existing.data.moTa ?? '', paymentTerms: existing.data.paymentTerms ?? '', budgetId: existing.data.budgetId ?? '', + // Auto-toggle manual mode khi load existing có manual data hoặc không có link + budgetManual: hasManual && !existing.data.budgetId, + budgetManualName: existing.data.budgetManualName ?? '', + budgetManualAmount: existing.data.budgetManualAmount ?? 0, }) } }, [existing.data]) + // Manual mode: clear budgetId, gửi manualName/Amount. Link mode: clear manual. + const payloadBudgetFields = form.budgetManual + ? { + budgetId: null, + budgetManualName: form.budgetManualName || null, + budgetManualAmount: form.budgetManualAmount > 0 ? form.budgetManualAmount : null, + } + : { + budgetId: form.budgetId || null, + budgetManualName: null, + budgetManualAmount: null, + } + const mut = useMutation({ mutationFn: async () => { if (editId) { @@ -89,7 +112,7 @@ export function PeHeaderForm({ diaDiem: form.diaDiem || null, moTa: form.moTa || null, paymentTerms: form.paymentTerms || null, - budgetId: form.budgetId || null, + ...payloadBudgetFields, }) } return api.post<{ id: string }>('/purchase-evaluations', { @@ -99,7 +122,7 @@ export function PeHeaderForm({ diaDiem: form.diaDiem || null, moTa: form.moTa || null, paymentTerms: form.paymentTerms || null, - budgetId: form.budgetId || null, + ...payloadBudgetFields, }) }, onSuccess: res => { @@ -161,26 +184,69 @@ export function PeHeaderForm({
- - -

- {!form.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.'} -

+
+ + {/* Toggle "Nhập tay" — Mig 17 fallback khi không link Budget entity */} + +
+ {!form.budgetManual ? ( + <> + +

+ {!form.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 — bật "Nhập tay" để nhập số tiền trực tiếp.' + : 'Chỉ list ngân sách đã duyệt cùng dự án.'} +

+ + ) : ( +
+
+ + setForm({ ...form, budgetManualName: e.target.value })} + placeholder="vd Tạm tính dự toán T11/2025" + maxLength={200} + /> +
+
+ + setForm({ ...form, budgetManualAmount: Number(e.target.value) })} + placeholder="1000000000" + /> + {form.budgetManualAmount > 0 && ( +

+ ≈ {form.budgetManualAmount.toLocaleString('vi-VN')} đ +

+ )} +
+
+ )}
diff --git a/fe-user/src/pages/contracts/ContractCreatePage.tsx b/fe-user/src/pages/contracts/ContractCreatePage.tsx index 6d3d889..a1d4a3e 100644 --- a/fe-user/src/pages/contracts/ContractCreatePage.tsx +++ b/fe-user/src/pages/contracts/ContractCreatePage.tsx @@ -305,6 +305,10 @@ function ContractHeaderForm({ const [noiDung, setNoiDung] = useState('') const [bypass, setBypass] = useState(false) const [budgetId, setBudgetId] = useState('') + // Mig 17 — manual budget fallback (toggle "Nhập tay") + const [budgetManual, setBudgetManual] = useState(false) + const [budgetManualName, setBudgetManualName] = useState('') + const [budgetManualAmount, setBudgetManualAmount] = useState(0) // Reset type về default khi typeFilter (parent prop) thay đổi useEffect(() => { setType(defaultType) }, [defaultType]) @@ -334,6 +338,11 @@ function ContractHeaderForm({ }) const qc = useQueryClient() + // Manual mode: clear budgetId, gửi manualName/Amount. Link mode: clear manual. + const budgetPayload = budgetManual + ? { budgetId: null, budgetManualName: budgetManualName || null, budgetManualAmount: budgetManualAmount > 0 ? budgetManualAmount : null } + : { budgetId: budgetId || null, budgetManualName: null, budgetManualAmount: null } + const create = useMutation({ mutationFn: async () => { const res = await api.post<{ id: string }>('/contracts', { @@ -347,7 +356,7 @@ function ContractHeaderForm({ noiDung: noiDung || null, bypassProcurementAndCCM: bypass, draftData: null, - budgetId: budgetId || null, + ...budgetPayload, }) return res.data.id }, @@ -387,26 +396,69 @@ function ContractHeaderForm({ 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.'} -

+
+ + {/* Toggle "Nhập tay" — Mig 17 fallback khi không link Budget entity */} + +
+ {!budgetManual ? ( + <> + +

+ {!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 — bật "Nhập tay" để nhập số tiền trực tiếp.' + : 'Chỉ list ngân sách đã duyệt cùng dự án.'} +

+ + ) : ( +
+
+ + setBudgetManualName(e.target.value)} + placeholder="vd Tạm tính dự toán T11/2025" + maxLength={200} + /> +
+
+ + setBudgetManualAmount(Number(e.target.value))} + placeholder="1000000000" + /> + {budgetManualAmount > 0 && ( +

+ ≈ {budgetManualAmount.toLocaleString('vi-VN')} đ +

+ )} +
+
+ )}