[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:
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user