[CLAUDE] FE-User: PE + HĐ toggle "Nhập tay" + 2 fields manual budget mirror fe-admin

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) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-07 12:40:29 +07:00
parent bab503189a
commit 14f8d9d808
6 changed files with 268 additions and 260 deletions

View File

@ -343,6 +343,17 @@ function ChonNccSection({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly
{' · '}{ev.budget.tenNganSach}
{' · '}<span className="text-slate-500">{ev.budget.tongNganSach.toLocaleString('vi-VN')} đ</span>
</a>
) : 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).
<span className="text-slate-700">
{ev.budgetManualName && <span>{ev.budgetManualName}</span>}
{ev.budgetManualName && ev.budgetManualAmount != null && ' · '}
{ev.budgetManualAmount != null && (
<span className="font-semibold text-slate-900">{ev.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>
</span>
) : <span className="text-slate-400"> (chưa link)</span>}
/>
<FormRow

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>