[CLAUDE] PurchaseEvaluation: ngan sach goi thau theo Excel anh Kiet - bang tong hop 2 block + nhap theo role PRO/CCM + xoa module Budget cu (Mig 50)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m31s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m31s
- Mig 50 ReplaceBudgetModuleWithPeWorkItemBudgets: bang moi PeWorkItemBudgets (1 record/cap Du an x Hang muc, UNIQUE filtered [IsDeleted]=0) + drop 5 bang Budget cu + PE/Contracts drop BudgetId + backfill BudgetManualAmount->BudgetPeriodAmount TRUOC DropColumn (phieu UAT giu so) + DELETE menu/permission Bg_* IN-list children-first
- BE: PUT {id}/budget/pro (role Procurement) + {id}/budget/ccm (role CostControl, Adjustment cho phep AM) fail-closed Forbidden-truoc-side-effect + EnsureTrackedAsync race-safe (catch unique -> re-fetch winner, loi khac rethrow) + auto-create record khi tao phieu + budgetSummary DTO (luy ke trinh-truoc/chon-thau-truoc/de-xuat-ky-nay + full fallback du-tru-PRO + canEdit flags) + submit-guard (3) doi predicate BudgetPeriodAmount -> "chua nhap Ngan sach ky nay" + PATCH budget-adjust absolute-set 2 field moi + Contract GIU BudgetManual* (HD nhap tay khong doi) + ke thua HD map BudgetPeriodAmount
- FE x2 app SHA256 identical: bang "TONG HOP NGAN SACH TRINH KY" block A (full dam + ban hanh + V0 hieu chinh + du tru PRO + ghi chu, editable theo canEditPro/canEditCcm) + block B 9 dong cong thuc Excel (5=1+3, 6=2+4, 7=full-5, 8 tu nhap default 7, 9=4+8) + to mau vuot ngan sach #C00000 / am do / red-soft row8>row7 + "Chua chon" khi count=0 + banner phieu chua gan Hang muc + o "Ngan sach ky nay" o create/header + XOA pages/components/types budgets + routes + menuKeys + Layout staticMap 4-place
- Tests: +22 PeWorkItemBudgetTests (auto-create x3, ensure/race x2, authz matrix PRO x5 + CCM x3, budgetSummary aggregates x5, adjust x4) - 14 BudgetPolicyTests xoa theo module - 1 test via-BudgetId -> 263 PASS (45 Domain + 218 Infra, 0 fail)
- database-agent advise adopted: khong FK vat ly PE/Contracts->Budgets (DropColumn khong can DropForeignKey) + DropIndex truoc DropColumn (SQL 5074) + IN-list thay LIKE Bg_% (underscore wildcard + miss root) + khong Serializable wrap (nested-tx conflict codegen)
- Reviewer PASS-with-minor 0 blocker (verdict-first survived); 2 minor da sua truoc commit (comment adjustMut absolute-set + dead key budgetId); note: F4 approver-edit-budget UI entry tam drafter-only, BE van cho approver scope - cho UAT anh Kiet
- Scaffold-bug caught: EF tu sinh RenameColumn BudgetManualAmount->ExpectedRemainingAmount (SAI semantics) -> thay bang Add+UPDATE+Drop
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -18,8 +18,7 @@ import {
|
||||
PurchaseEvaluationTypeLabel,
|
||||
type PeDetailBundle,
|
||||
} from '@/types/purchaseEvaluation'
|
||||
import { BudgetPhase, type BudgetListItem } from '@/types/budget'
|
||||
import type { Paged, Project } from '@/types/master'
|
||||
import type { Project } from '@/types/master'
|
||||
|
||||
// VND format helpers (mirror PeDetailTabs.tsx — session 20)
|
||||
const parseVnd = (s: string): number => Number(s.replace(/[^\d]/g, '')) || 0
|
||||
@ -81,29 +80,13 @@ export function PeHeaderForm({
|
||||
diaDiem: '',
|
||||
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({
|
||||
queryKey: ['eligible-budgets', form.projectId],
|
||||
queryFn: async () => {
|
||||
const res = await api.get<Paged<BudgetListItem>>('/budgets', {
|
||||
params: { pageSize: 100, projectId: form.projectId, phase: BudgetPhase.DaDuyet },
|
||||
})
|
||||
return res.data.items
|
||||
},
|
||||
enabled: !!form.projectId,
|
||||
// [S61 Mig 50] "Ngân sách - kỳ này" — thay budgetId/budgetManual* cũ (module
|
||||
// Budget xóa hẳn; bảng Tổng hợp ngân sách gói thầu nằm ở PeDetailTabs).
|
||||
budgetPeriodAmount: 0,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (existing.data) {
|
||||
// S59: manual-mode detect theo CẢ amount (phiếu mới name=null sau khi bỏ ô Tên).
|
||||
const hasManual = existing.data.budgetManualAmount !== null || existing.data.budgetManualName !== null
|
||||
|| existing.data.budgetManualAmount !== null
|
||||
setForm({
|
||||
type: existing.data.type,
|
||||
tenGoiThau: existing.data.tenGoiThau,
|
||||
@ -112,27 +95,16 @@ export function PeHeaderForm({
|
||||
diaDiem: existing.data.diaDiem ?? '',
|
||||
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,
|
||||
budgetPeriodAmount: existing.data.budgetPeriodAmount ?? 0,
|
||||
})
|
||||
}
|
||||
}, [existing.data])
|
||||
|
||||
// Manual mode: clear budgetId, gửi manualName/Amount. Link mode: clear manual.
|
||||
const payloadBudgetFields = form.budgetManual
|
||||
? {
|
||||
budgetId: null,
|
||||
budgetManualName: null, // S59 anh chốt bỏ "Tên ngân sách" — manual chỉ còn Số tiền
|
||||
budgetManualAmount: form.budgetManualAmount > 0 ? form.budgetManualAmount : null,
|
||||
}
|
||||
: {
|
||||
budgetId: form.budgetId || null,
|
||||
budgetManualName: null,
|
||||
budgetManualAmount: null,
|
||||
}
|
||||
// [S61] PUT UpdateDraft null-safe: budgetPeriodAmount null = GIỮ giá trị cũ
|
||||
// BE-side; gửi số > 0 mới set. (Clear hẳn → dùng bảng Tổng hợp/budget-adjust.)
|
||||
const payloadBudgetFields = {
|
||||
budgetPeriodAmount: form.budgetPeriodAmount > 0 ? form.budgetPeriodAmount : null,
|
||||
}
|
||||
|
||||
const mut = useMutation({
|
||||
mutationFn: async () => {
|
||||
@ -239,58 +211,23 @@ export function PeHeaderForm({
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<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>
|
||||
{/* [S61 Mig 50] Ô đơn "Ngân sách kỳ này" — thay picker Budget cũ + toggle
|
||||
nhập tay. Số phân bổ cho RIÊNG phiếu này (row 3 bảng Tổng hợp). */}
|
||||
<Label>Ngân sách kỳ này</Label>
|
||||
<div className="relative max-w-xs">
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={formatVndInput(form.budgetPeriodAmount)}
|
||||
onChange={e => setForm({ ...form, budgetPeriodAmount: parseVnd(e.target.value) })}
|
||||
placeholder="0"
|
||||
className="pr-10 font-mono text-right"
|
||||
/>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-[12px] font-medium text-slate-500">đ</span>
|
||||
</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>
|
||||
<Label className="text-[11px]">Số tiền</Label>
|
||||
<div className="relative max-w-xs">
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={formatVndInput(form.budgetManualAmount)}
|
||||
onChange={e => setForm({ ...form, budgetManualAmount: parseVnd(e.target.value) })}
|
||||
placeholder="0"
|
||||
className="pr-10 font-mono text-right"
|
||||
/>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-[12px] font-medium text-slate-500">đ</span>
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-slate-500">VND — nhập số, tự format dấu chấm ngàn (vd 1.000.000)</p>
|
||||
</div>
|
||||
)}
|
||||
<p className="mt-1 text-[11px] text-slate-500">
|
||||
Số phân bổ cho riêng phiếu này — bắt buộc trước khi gửi duyệt. Ngân sách full gói thầu xem ở bảng "Tổng hợp ngân sách trình ký" trong phiếu.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user