[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

@ -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}
/>
<div className="mt-4 space-y-1.5">
<Label>Ngân sách (đi chiếu chi phí)</Label>
<Select
value={budgetId}
disabled={!projectId}
onChange={e => setBudgetId(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="text-[11px] text-slate-500">
{!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="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={budgetManual}
onChange={e => setBudgetManual(e.target.checked)}
className="h-3.5 w-3.5 rounded border-slate-300"
/>
Nhập tay (không link)
</label>
</div>
{!budgetManual ? (
<>
<Select
value={budgetId}
disabled={!projectId}
onChange={e => setBudgetId(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="text-[11px] text-slate-500">
{!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={budgetManualName}
onChange={e => setBudgetManualName(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={budgetManualAmount || ''}
onChange={e => setBudgetManualAmount(Number(e.target.value))}
placeholder="1000000000"
/>
{budgetManualAmount > 0 && (
<p className="mt-1 text-[11px] text-slate-500">
{budgetManualAmount.toLocaleString('vi-VN')} đ
</p>
)}
</div>
</div>
)}
</div>
<div className="mt-4 flex justify-end gap-2">
<Button type="submit" disabled={create.isPending}>
@ -497,6 +549,11 @@ function ContractEditForm({
const [tenHopDong, setTenHopDong] = useState(contract.tenHopDong ?? '')
const [noiDung, setNoiDung] = useState(contract.noiDung ?? '')
const [budgetId, setBudgetId] = useState(contract.budgetId ?? '')
// Mig 17 — manual budget fallback. Auto-toggle khi load có manual data
const hasInitialManual = contract.budgetManualName !== null || contract.budgetManualAmount !== null
const [budgetManual, setBudgetManual] = useState(hasInitialManual && !contract.budgetId)
const [budgetManualName, setBudgetManualName] = useState(contract.budgetManualName ?? '')
const [budgetManualAmount, setBudgetManualAmount] = useState(contract.budgetManualAmount ?? 0)
const templates = useQuery({
queryKey: ['templates-by-type', contract.type],
@ -513,6 +570,10 @@ function ContractEditForm({
})
const qc = useQueryClient()
const budgetPayload = budgetManual
? { budgetId: null, budgetManualName: budgetManualName || null, budgetManualAmount: budgetManualAmount > 0 ? budgetManualAmount : null }
: { budgetId: budgetId || null, budgetManualName: null, budgetManualAmount: null }
const update = useMutation({
mutationFn: async () => {
await api.put(`/contracts/${contract.id}`, {
@ -522,7 +583,7 @@ function ContractEditForm({
noiDung: noiDung || null,
templateId: templateId || null,
draftData: null,
budgetId: budgetId || null,
...budgetPayload,
})
},
onSuccess: () => {
@ -611,23 +672,65 @@ function ContractEditForm({
<Textarea rows={4} value={noiDung} onChange={e => setNoiDung(e.target.value)} disabled={!isDraft} />
</div>
<div className="col-span-2 space-y-1.5">
<Label>Ngân sách (đi chiếu chi phí)</Label>
<div className="flex items-center justify-between">
<Label className="mb-0">Ngân sách (đi chiếu chi phí)</Label>
{isDraft && (
<label className="inline-flex cursor-pointer items-center gap-1.5 text-[11px] text-slate-600">
<input
type="checkbox"
checked={budgetManual}
onChange={e => setBudgetManual(e.target.checked)}
className="h-3.5 w-3.5 rounded border-slate-300"
/>
Nhập tay (không link)
</label>
)}
</div>
{isDraft ? (
<>
<Select value={budgetId} onChange={e => setBudgetId(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="text-[11px] text-slate-500">
{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>
</>
!budgetManual ? (
<>
<Select value={budgetId} onChange={e => setBudgetId(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="text-[11px] text-slate-500">
{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={budgetManualName}
onChange={e => setBudgetManualName(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={budgetManualAmount || ''}
onChange={e => setBudgetManualAmount(Number(e.target.value))}
placeholder="1000000000"
/>
{budgetManualAmount > 0 && (
<p className="mt-1 text-[11px] text-slate-500">
{budgetManualAmount.toLocaleString('vi-VN')} đ
</p>
)}
</div>
</div>
)
) : contract.budget ? (
<a
href={`/budgets?id=${contract.budget.id}`}
@ -637,6 +740,16 @@ function ContractEditForm({
{' · '}{contract.budget.tenNganSach}
{' · '}<span className="text-slate-500">{contract.budget.tongNganSach.toLocaleString('vi-VN')} đ</span>
</a>
) : contract.budgetManualAmount != null || contract.budgetManualName ? (
// Mig 17 — read-only display khi !isDraft + có manual data
<div className="block rounded border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-700">
{contract.budgetManualName && <span>{contract.budgetManualName}</span>}
{contract.budgetManualName && contract.budgetManualAmount != null && ' · '}
{contract.budgetManualAmount != null && (
<span className="font-semibold text-slate-900">{contract.budgetManualAmount.toLocaleString('vi-VN')} đ</span>
)}
<span className="ml-2 rounded bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-500">nhập tay</span>
</div>
) : (
<Input value="(không link)" disabled className="bg-slate-50" />
)}