[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

- 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:
pqhuy1987
2026-06-13 01:07:27 +07:00
parent 6db195dd42
commit 79ef8da9f4
70 changed files with 9052 additions and 5956 deletions

View File

@ -18,8 +18,7 @@ import { Select } from '@/components/ui/Select'
import { api } from '@/lib/api'
import { getErrorMessage } from '@/lib/apiError'
import { PurchaseEvaluationTypeLabel } 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
@ -59,11 +58,9 @@ export function PeWorkspaceCreateView({
diaDiem: '',
moTa: '',
paymentTerms: '',
budgetId: '',
// Mig 17 — manual budget fallback
budgetManual: false,
budgetManualName: '',
budgetManualAmount: 0,
// [S61 Mig 50] "Ngân sách - kỳ này" — thay budgetId/budgetManual* (module
// Budget cũ xóa hẳn; bảng Tổng hợp ngân sách gói thầu ở PeDetailTabs).
budgetPeriodAmount: 0,
// Mig 23 — Pin quy trình duyệt V2 (User tự chọn lúc tạo)
approvalWorkflowId: '',
})
@ -104,20 +101,9 @@ export function PeWorkspaceCreateView({
},
})
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,
})
const budgetPayload = form.budgetManual
? { budgetId: null, budgetManualName: form.budgetManualName || null, budgetManualAmount: form.budgetManualAmount > 0 ? form.budgetManualAmount : null }
: { budgetId: form.budgetId || null, budgetManualName: null, budgetManualAmount: null }
const budgetPayload = {
budgetPeriodAmount: form.budgetPeriodAmount > 0 ? form.budgetPeriodAmount : null,
}
const create = useMutation({
mutationFn: async () => {
@ -224,7 +210,7 @@ export function PeWorkspaceCreateView({
const loc = p?.location ?? ''
setForm(f => {
const untouched = !f.diaDiem || f.diaDiem === lastAutoLoc.current
return { ...f, projectId: id, budgetId: '', diaDiem: untouched ? loc : f.diaDiem }
return { ...f, projectId: id, diaDiem: untouched ? loc : f.diaDiem }
})
lastAutoLoc.current = loc
}}
@ -258,55 +244,26 @@ export function PeWorkspaceCreateView({
value={<span className="text-slate-400"> (sau khi thêm NCC tham gia + chốt winner)</span>}
/>
{/* b. Ngân sách — editable inline (Mig 17 toggle pattern) */}
{/* [S61 Mig 50] b. Ngân sách kỳ này — ô đơn thay picker Budget cũ +
toggle nhập tay (module Budget xóa hẳn). Số phân bổ cho RIÊNG
phiếu này (row 3 bảng "Tổng hợp ngân sách trình ký"). */}
<div className="flex gap-3">
<span className="w-44 shrink-0 pt-1.5 text-[12px] text-slate-500">b. Ngân sách</span>
<div className="min-w-0 flex-1 space-y-2">
<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"
<span className="w-44 shrink-0 pt-1.5 text-[12px] text-slate-500">b. Ngân sách kỳ này</span>
<div className="min-w-0 flex-1 space-y-1">
<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 text-sm"
/>
Nhập tay (không link)
</label>
{!form.budgetManual ? (
<>
<Select
value={form.budgetId}
disabled={!form.projectId}
onChange={e => setForm({ ...form, budgetId: e.target.value })}
className="text-sm"
>
<option value=""></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">
{!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="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 text-sm"
/>
<span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-[12px] font-medium text-slate-500">đ</span>
</div>
)}
<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="text-[11px] text-slate-500">
Bắt buộc trước khi gửi duyệt. Ngân sách full gói thầu xem bảng &ldquo;Tổng hợp ngân sách trình &rdquo; sau khi tạo phiếu.
</p>
</div>
</div>