From 14f8d9d808ef1f154a8261991ecea09a4f2dc8c7 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Thu, 7 May 2026 12:40:29 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20FE-User:=20PE=20+=20H=C4=90=20toggle?= =?UTF-8?q?=20"Nh=E1=BA=ADp=20tay"=20+=202=20fields=20manual=20budget=20mi?= =?UTF-8?q?rror=20fe-admin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chunk 4/5 — mirror y hệt Chunk 3 sang fe-user (rule §3.9 duplicate có chủ đích). Files: ~ fe-user/src/types/purchaseEvaluation.ts — PeDetailBundle +2 field ~ fe-user/src/types/contracts.ts — ContractDetail +2 field ~ fe-user/src/components/pe/PeHeaderForm.tsx (copy từ fe-admin) ~ fe-user/src/components/pe/PeDetailTabs.tsx — Section "b. Ngân sách" fallback display khi !ev.budget + có manual data ~ fe-user/src/pages/pe/PurchaseEvaluationCreatePage.tsx (copy refactor wrap) ~ fe-user/src/pages/contracts/ContractCreatePage.tsx — toggle pattern cho NewContractForm + EditContractForm (giống fe-admin) Verify: npm run build fe-user pass · 1904 modules · 0 TS error. Next: Chunk 5 docs + push. Co-Authored-By: Claude Opus 4.7 (1M context) --- fe-user/src/components/pe/PeDetailTabs.tsx | 11 + fe-user/src/components/pe/PeHeaderForm.tsx | 110 +++++++-- .../pages/contracts/ContractCreatePage.tsx | 189 ++++++++++++---- .../pages/pe/PurchaseEvaluationCreatePage.tsx | 212 +----------------- fe-user/src/types/contracts.ts | 3 + fe-user/src/types/purchaseEvaluation.ts | 3 + 6 files changed, 268 insertions(+), 260 deletions(-) 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')} đ +

+ )} +
+
+ )}