[CLAUDE] FE-Admin: PE + HĐ toggle "Nhập tay" + 2 fields manual budget (Mig 17)

Chunk 3/5 — UI cho manual budget fallback Mig 17. Toggle checkbox "Nhập tay
(không link)" cạnh Label Ngân sách. Khi ON: hide Select Budget, show 2 input
field grid 2-col (Tên tham chiếu text + Số tiền number formatted VND).

Files sửa:
  ~ fe-admin/src/types/purchaseEvaluation.ts — PeDetailBundle +2 field
  ~ fe-admin/src/types/contracts.ts — ContractDetail +2 field
  ~ fe-admin/src/components/pe/PeHeaderForm.tsx — toggle + 2 input + payload
    conditional (manual mode → clear budgetId, link mode → clear manual). Auto-
    detect manual mode khi load existing PE có manual data + !budgetId.
  ~ fe-admin/src/components/pe/PeDetailTabs.tsx — Section 2 "b. Ngân sách"
    fallback display khi !ev.budget + có manual data: render text "Tên · Số tiền"
    + badge "nhập tay" thay vì "(chưa link)".
  ~ fe-admin/src/pages/pe/PurchaseEvaluationCreatePage.tsx — refactor wrap
    PeHeaderForm để DRY (auto-inherit toggle pattern, không drift). 222 LOC → 30 LOC.
  ~ fe-admin/src/pages/contracts/ContractCreatePage.tsx — apply same toggle
    pattern cho cả NewContractForm + EditContractForm. EditForm thêm read-only
    display branch khi !isDraft + có manual data.

Verify: npm run build fe-admin pass · 1922 modules · không TS error.

Next: Chunk 4 fe-user mirror (PeHeaderForm + PeDetailTabs + ContractCreatePage
+ types).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-07 12:37:42 +07:00
parent 0f7901c19f
commit bab503189a
6 changed files with 268 additions and 260 deletions

View File

@ -53,6 +53,10 @@ export function PeHeaderForm({
moTa: '',
paymentTerms: '',
budgetId: '' as string,
// Mig 17 — manual budget fallback (toggle "Nhập tay" thay vì link)
budgetManual: false,
budgetManualName: '',
budgetManualAmount: 0,
})
const eligibleBudgets = useQuery({
@ -68,6 +72,8 @@ export function PeHeaderForm({
useEffect(() => {
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({
</div>
<div>
<Label>Ngân sách (đi chiếu chi phí)</Label>
<Select
value={form.budgetId}
disabled={!form.projectId}
onChange={e => setForm({ ...form, budgetId: e.target.value })}
>
<option value=""> (không link)</option>
{eligibleBudgets.data?.map(b => (
<option key={b.id} value={b.id}>
{b.maNganSach ?? '—'} · {b.tenNganSach} · {b.tongNganSach.toLocaleString('vi-VN')} đ
</option>
))}
</Select>
<p className="mt-1 text-[11px] text-slate-500">
{!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.'}
</p>
<div className="mb-1.5 flex items-center justify-between">
<Label className="mb-0">Ngân sách (đi chiếu chi phí)</Label>
{/* Toggle "Nhập tay" — Mig 17 fallback khi không link Budget entity */}
<label className="inline-flex cursor-pointer items-center gap-1.5 text-[11px] text-slate-600">
<input
type="checkbox"
checked={form.budgetManual}
onChange={e => setForm({ ...form, budgetManual: e.target.checked })}
className="h-3.5 w-3.5 rounded border-slate-300"
/>
Nhập tay (không link)
</label>
</div>
{!form.budgetManual ? (
<>
<Select
value={form.budgetId}
disabled={!form.projectId}
onChange={e => setForm({ ...form, budgetId: e.target.value })}
>
<option value=""> (không link)</option>
{eligibleBudgets.data?.map(b => (
<option key={b.id} value={b.id}>
{b.maNganSach ?? '—'} · {b.tenNganSach} · {b.tongNganSach.toLocaleString('vi-VN')} đ
</option>
))}
</Select>
<p className="mt-1 text-[11px] text-slate-500">
{!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.'}
</p>
</>
) : (
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
<div>
<Label className="text-[11px]">Tên ngân sách (tham chiếu)</Label>
<Input
value={form.budgetManualName}
onChange={e => setForm({ ...form, budgetManualName: e.target.value })}
placeholder="vd Tạm tính dự toán T11/2025"
maxLength={200}
/>
</div>
<div>
<Label className="text-[11px]">Số tiền (đ)</Label>
<Input
type="number"
min={0}
value={form.budgetManualAmount || ''}
onChange={e => setForm({ ...form, budgetManualAmount: Number(e.target.value) })}
placeholder="1000000000"
/>
{form.budgetManualAmount > 0 && (
<p className="mt-1 text-[11px] text-slate-500">
{form.budgetManualAmount.toLocaleString('vi-VN')} đ
</p>
)}
</div>
</div>
)}
</div>
<div>