[CLAUDE] PurchaseEvaluation: Mig 55 ô "Ghi chú từ CCM" ngân sách gói thầu — CCM nhập số + ghi lý do giống PRO
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m54s

- Entity PeWorkItemBudget +CcmNote (mirror ProNote, nvarchar 1000) + Mig 55 additive-nullable
- UpdatePeBudgetCcmCommand +CcmNote absolute-set, role-gate CostControl/Admin fail-closed
- DTO PeBudgetSummaryDto +CcmNote + controller BudgetCcmBody + GET mapping
- FE 2 app SHA-mirror: dòng "Ghi chú từ CCM" gate canEditCcm (sau V0/hiệu chỉnh), absolute-set đủ 3 field
- Test +5 (set CCM/Admin, null-clear, non-priv Forbidden, all-3-persist) -> 339 pass

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-18 19:05:10 +07:00
parent e7e99d10f2
commit 8655ebf1ba
14 changed files with 6487 additions and 15 deletions

View File

@ -1079,7 +1079,7 @@ function PeBudgetSummaryTable({ ev, readOnly }: { ev: PeDetailBundle; readOnly:
})
// PUT /budget/ccm — chỉ khi canEditCcm. initialAmount + adjustmentAmount.
const ccmMut = useMutation({
mutationFn: async (body: { initialAmount: number | null; adjustmentAmount: number | null }) =>
mutationFn: async (body: { initialAmount: number | null; adjustmentAmount: number | null; ccmNote: string | null }) =>
api.put(`/purchase-evaluations/${ev.id}/budget/ccm`, body),
onSuccess: () => { toast.success('Đã lưu ngân sách ban hành'); invalidate() },
onError: e => toast.error(getErrorMessage(e)),
@ -1097,6 +1097,9 @@ function PeBudgetSummaryTable({ ev, readOnly }: { ev: PeDetailBundle; readOnly:
// proNote inline-edit state (Textarea — không dùng VndInlineEdit)
const [proNoteText, setProNoteText] = useState(bs?.proNote ?? '')
useEffect(() => { setProNoteText(bs?.proNote ?? '') }, [bs?.proNote])
// ccmNote inline-edit state (mirror proNoteText) — [Mig anh Kiệt FDC]
const [ccmNoteText, setCcmNoteText] = useState(bs?.ccmNote ?? '')
useEffect(() => { setCcmNoteText(bs?.ccmNote ?? '') }, [bs?.ccmNote])
// Phiếu cũ chưa gắn Hạng mục công việc → budgetSummary null.
if (!bs) {
@ -1172,7 +1175,7 @@ function PeBudgetSummaryTable({ ev, readOnly }: { ev: PeDetailBundle; readOnly:
initial={bs.initialAmount}
saving={ccmMut.isPending}
label="Ngân sách ban hành lần đầu"
onSave={v => ccmMut.mutate({ initialAmount: v, adjustmentAmount: bs.adjustmentAmount })}
onSave={v => ccmMut.mutate({ initialAmount: v, adjustmentAmount: bs.adjustmentAmount, ccmNote: bs.ccmNote })}
/>
) : bs.initialAmount != null ? fmtVnd(bs.initialAmount) : <span className="text-slate-400"></span>
}
@ -1188,7 +1191,7 @@ function PeBudgetSummaryTable({ ev, readOnly }: { ev: PeDetailBundle; readOnly:
allowNegative
saving={ccmMut.isPending}
label="Ngân sách hiệu chỉnh tăng giảm"
onSave={v => ccmMut.mutate({ initialAmount: bs.initialAmount, adjustmentAmount: v })}
onSave={v => ccmMut.mutate({ initialAmount: bs.initialAmount, adjustmentAmount: v, ccmNote: bs.ccmNote })}
/>
) : bs.adjustmentAmount != null ? (
<span className={cn(bs.adjustmentAmount < 0 && 'text-red-600')}>{fmtVndSigned(bs.adjustmentAmount)}</span>
@ -1196,6 +1199,37 @@ function PeBudgetSummaryTable({ ev, readOnly }: { ev: PeDetailBundle; readOnly:
}
/>
{/* Ghi chú từ CCM (CCM editable — Textarea, mirror Ghi chú từ PRO) — [Mig anh Kiệt FDC] */}
<div className="flex items-start gap-2 border-b border-slate-100 px-3 py-1.5 text-[13px]">
<div className="min-w-0 flex-1 text-slate-700">Ghi chú từ CCM</div>
<div className="w-72 shrink-0">
{bs.canEditCcm ? (
<div className="space-y-1">
<textarea
value={ccmNoteText}
onChange={e => setCcmNoteText(e.target.value)}
placeholder="Ghi chú ngân sách CCM…"
rows={2}
className="w-full rounded border border-slate-300 px-2 py-1 text-[12px]"
/>
<div className="flex justify-end">
<Button
onClick={() => ccmMut.mutate({ initialAmount: bs.initialAmount, adjustmentAmount: bs.adjustmentAmount, ccmNote: ccmNoteText || null })}
disabled={ccmNoteText === (bs.ccmNote ?? '') || ccmMut.isPending}
className="h-6 px-2 text-[11px]"
>
{ccmMut.isPending ? '…' : 'Lưu ghi chú'}
</Button>
</div>
</div>
) : (
<div className="whitespace-pre-wrap text-right text-[12px] text-slate-600">
{bs.ccmNote || <span className="text-slate-400"></span>}
</div>
)}
</div>
</div>
{/* Dòng 4 — Dự trù PRO (PRO editable) */}
<BudgetRow
label="Ngân sách PRO"

View File

@ -295,6 +295,7 @@ export type PeBudgetSummary = {
proNote: string | null
initialAmount: number | null
adjustmentAmount: number | null // CCM "NS V0/hiệu chỉnh" — cho phép ÂM
ccmNote: string | null // [Mig — anh Kiệt FDC] Ghi chú từ CCM (mirror proNote)
fullAmount: number
fullIsEstimate: boolean
canEditPro: boolean